【QML 界面开发实战之:模块化、多QML文件调用与跨语言交互】

在 Qt 开发中,QML 以其直观的声明式语法、高效的界面渲染和丰富的组件库,成为现代跨平台应用界面开发的首选。然而,随着应用复杂度的提升,如何实现界面的模块化拆分、多文件协同工作、屏幕切换管理以及与 C++ 业务逻辑的无缝交互,成为开发者必须攻克的核心问题。本文结合实际开发场景,详细拆解这些关键技术点,提供可直接落地的解决方案和完整代码示例。

一、界面模块化:从 "单体文件" 到 "组件化架构"

1. 模块化的核心价值

传统的 QML 开发常将所有界面元素写在一个 main.qml 文件中,导致代码臃肿、维护困难、复用性差。模块化开发通过将界面拆分为独立的、可复用的组件,带来三大优势:

  • 可维护性:单个组件职责单一,代码量减少,便于定位和修改问题;
  • 复用性:封装后的组件可在项目任意位置调用,避免重复开发;
  • 团队协作:不同开发者可并行开发不同组件,提升开发效率。

2. 模块化实现的核心原则

在 QML 中,模块化的核心是 "自定义组件" ------ 将某一功能模块(如按钮、列表项、弹窗)封装为独立的 QML 文件,通过 import 语句在其他文件中引用。实现时需遵循以下原则:

  • 单一职责:一个组件只负责一个核心功能(如 CustomButton.qml 仅处理按钮的显示和交互);
  • 属性暴露:通过 property 关键字暴露组件的可配置属性(如按钮的文本、颜色、点击回调);
  • 信号与槽:通过 signal 定义组件的交互信号,外部通过 on 响应;
  • 样式隔离:组件内部定义自身样式,避免与外部样式冲突。

3. 自定义组件实战:封装通用按钮

以封装一个支持自定义文本、颜色和点击事件的通用按钮为例,步骤如下:

  • 步骤 1:创建独立 QML 文件
    在项目中创建 components 目录(用于存放所有自定义组件),新建 CustomButton.qml 文件:
css 复制代码
// components/CustomButton.qml
import QtQuick 2.14
import QtQuick.Controls 2.14

Button {
    // 暴露可配置属性:按钮文本、背景色、文本色
    property string btnText: "默认按钮"
    property color bgColor: "#4CAF50"
    property color textColor: "white"

    // 绑定属性到组件内部
    text: btnText
    background: Rectangle {
        color: control.pressed ? Qt.darker(bgColor, 1.2) : bgColor // 按下时加深颜色
        radius: 4
    }
    label: Text {
        text: control.btnText
        color: control.textColor
        font.pointSize: 14
        font.bold: true
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
    }

    // 定义点击信号(可选,也可直接使用 Button 自带的 onClicked)
    signal btnClicked()

    // 触发信号
    onClicked: {
        btnClicked()
    }
}
  • 步骤 2:在主界面中引用组件
    在 main.qml 中通过 import 引入自定义按钮,并使用:
css 复制代码
// main.qml
import QtQuick 2.14
import QtQuick.Window 2.14
import "./components" // 导入自定义组件目录(相对路径)

Window {
    width: 400
    height: 300
    visible: true
    title: "QML 模块化示例"

    // 使用自定义按钮
    CustomButton {
        x: 50
        y: 50
        btnText: "点击触发弹窗"
        bgColor: "#2196F3"
        textColor: "white"
        width: 150
        height: 40

        // 响应自定义信号
        onBtnClicked: {
            console.log("自定义按钮被点击")
            // 后续可添加弹窗逻辑
        }
    }

    // 复用自定义按钮,修改属性
    CustomButton {
        x: 50
        y: 120
        btnText: "切换界面"
        bgColor: "#FF9800"
        textColor: "black"
        width: 150
        height: 40

        onBtnClicked: {
            console.log("切换到第二屏")
            // 后续添加屏幕切换逻辑
        }
    }
}
    1. 复杂组件封装:列表项组件
      对于更复杂的组件(如列表中的每一项),可结合 ListView 和自定义组件实现复用。例如封装 ListItem.qml:
css 复制代码
// components/ListItem.qml
import QtQuick 2.14
import QtQuick.Controls 2.14

Item {
    width: parent.width
    height: 60
    property string title: "默认标题"
    property string subtitle: "默认副标题"
    property string iconSource: "qrc:/images/icon.png"

    Row {
        anchors.fill: parent
        spacing: 10
        anchors.margins: 10

        Image {
            width: 40
            height: 40
            source: iconSource
            fillMode: Image.KeepAspectFit
        }

        Column {
            anchors.verticalCenter: parent.verticalCenter
            spacing: 5

            Text {
                text: title
                font.pointSize: 14
                font.bold: true
                color: "#333"
            }

            Text {
                text: subtitle
                font.pointSize: 12
                color: "#666"
            }
        }

        // 右侧箭头
        Image {
            width: 20
            height: 20
            source: "qrc:/images/arrow_right.png"
            anchors.right: parent.right
            anchors.verticalCenter: parent.verticalCenter
        }
    }

    // 点击事件
    MouseArea {
        anchors.fill: parent
        onClicked: {
            console.log("列表项", title, "被点击")
        }
    }
}

在 ListView 中使用该组件:

css 复制代码
// main.qml 中添加
ListView {
    x: 50
    y: 200
    width: 300
    height: 200
    model: ListModel {
        ListElement { title: "消息通知"; subtitle: "您有3条未读消息"; iconSource: "qrc:/images/msg.png" }
        ListElement { title: "系统设置"; subtitle: "点击进入设置界面"; iconSource: "qrc:/images/setting.png" }
        ListElement { title: "关于我们"; subtitle: "版本号:1.0.0"; iconSource: "qrc:/images/about.png" }
    }
    delegate: ListItem {
        title: model.title
        subtitle: model.subtitle
        iconSource: model.iconSource
    }
}

二、多文件调用与屏幕切换:实现结构化界面导航

1. 多文件组织原则

当应用包含多个屏幕(如登录页、主界面、设置页)时,需将每一屏封装为独立的 QML 文件,按功能划分目录,例如:

bash 复制代码
project/
├── main.qml          # 入口文件,管理屏幕切换
├── screens/          # 屏幕目录
│   ├── LoginScreen.qml   # 登录页
│   ├── MainScreen.qml    # 主界面
│   └── SettingScreen.qml # 设置页
├── components/       # 自定义组件目录
│   ├── CustomButton.qml
│   └── ListItem.qml
└── images/           # 图片资源目录

2. 屏幕切换的核心方案

QML 中实现屏幕切换的核心是 "视图容器",Qt中我们常用两种方案:StackView(栈式切换,支持返回上一级)和 SwipView(滑动切换,适合平级界面)。以下详细讲解两种方案的实现。

3. 方案一:StackView 栈式切换(推荐用于多级界面)

StackView 以 "栈" 的形式管理界面,新界面从右侧推入栈顶,返回时从栈顶弹出,适合场景:登录页 → 主界面 → 设置界面(支持返回主界面)。

  • 步骤 1:入口文件 main.qml 中创建 StackView
css 复制代码
// main.qml
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14
import "./screens" // 导入屏幕目录

Window {
    width: 400
    height: 600
    visible: true
    title: "QML 屏幕切换示例"

    // StackView 容器,充满整个窗口
    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: LoginScreen {} // 初始显示登录页
    }
}
  • 步骤 2:实现登录页 LoginScreen.qml
css 复制代码
// screens/LoginScreen.qml
import QtQuick 2.14
import QtQuick.Controls 2.14
import "../components" // 导入自定义组件

Item {
    width: parent.width
    height: parent.height

    Column {
        anchors.centerIn: parent
        spacing: 20

        Text {
            text: "用户登录"
            font.pointSize: 20
            font.bold: true
            color: "#333"
        }

        TextField {
            width: 300
            placeholderText: "请输入用户名"
            font.pointSize: 14
        }

        TextField {
            width: 300
            placeholderText: "请输入密码"
            echoMode: TextField.Password
            font.pointSize: 14
        }

        // 使用自定义按钮
        CustomButton {
            btnText: "登录"
            bgColor: "#2196F3"
            width: 300
            height: 45

            onBtnClicked: {
                // 验证通过后,切换到主界面(推入栈顶)
                stackView.push("qrc:/screens/MainScreen.qml")
            }
        }
    }
}
  • 步骤 3:实现主界面 MainScreen.qml
css 复制代码
// screens/MainScreen.qml
import QtQuick 2.14
import QtQuick.Controls 2.14
import "../components"

Item {
    width: parent.width
    height: parent.height

    // 顶部导航栏
    Rectangle {
        width: parent.width
        height: 50
        color: "#2196F3"

        Text {
            text: "主界面"
            color: "white"
            font.pointSize: 16
            font.bold: true
            anchors.centerIn: parent
        }

        // 返回按钮(隐藏,主界面无需返回)
        CustomButton {
            visible: false
            x: 10
            y: 5
            width: 80
            height: 40
            btnText: "返回"
            bgColor: "transparent"
            textColor: "white"

            onBtnClicked: {
                stackView.pop() // 弹出栈顶,返回上一级
            }
        }
    }

    // 主内容区
    ListView {
        anchors.top: parent.top
        anchors.topMargin: 60
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        model: ListModel {
            ListElement { title: "消息中心"; subtitle: "3条未读"; icon: "qrc:/images/msg.png" }
            ListElement { title: "系统设置"; subtitle: "点击进入"; icon: "qrc:/images/setting.png" }
            ListElement { title: "个人中心"; subtitle: "编辑资料"; icon: "qrc:/images/profile.png" }
        }
        delegate: ListItem {
            title: model.title
            subtitle: model.subtitle
            iconSource: model.icon

            // 列表项点击事件
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    if (model.title === "系统设置") {
                        // 切换到设置界面(推入栈顶)
                        stackView.push("qrc:/screens/SettingScreen.qml")
                    }
                }
            }
        }
    }
}
  • 步骤 4:实现设置界面 SettingScreen.qml
css 复制代码
// screens/SettingScreen.qml
import QtQuick 2.14
import QtQuick.Controls 2.14
import "../components"

Item {
    width: parent.width
    height: parent.height

    // 顶部导航栏
    Rectangle {
        width: parent.width
        height: 50
        color: "#2196F3"

        Text {
            text: "系统设置"
            color: "white"
            font.pointSize: 16
            font.bold: true
            anchors.centerIn: parent
        }

        // 返回按钮
        CustomButton {
            x: 10
            y: 5
            width: 80
            height: 40
            btnText: "返回"
            bgColor: "transparent"
            textColor: "white"

            onBtnClicked: {
                stackView.pop() // 返回主界面
            }
        }
    }

    // 内容区
    Column {
        anchors.top: parent.top
        anchors.topMargin: 60
        anchors.left: parent.left
        anchors.right: parent.right
        spacing: 1

        // 设置项
        Rectangle {
            width: parent.width
            height: 50
            color: "white"

            Row {
                anchors.fill: parent
                spacing: 10
                anchors.margins: 15

                Text {
                    text: "通知设置"
                    font.pointSize: 14
                    color: "#333"
                    anchors.verticalCenter: parent.verticalCenter
                }

                Switch {
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    checked: true
                }
            }
        }

        Rectangle {
            width: parent.width
            height: 50
            color: "white"

            Row {
                anchors.fill: parent
                spacing: 10
                anchors.margins: 15

                Text {
                    text: "夜间模式"
                    font.pointSize: 14
                    color: "#333"
                    anchors.verticalCenter: parent.verticalCenter
                }

                Switch {
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    checked: false
                }
            }
        }

        // 退出登录按钮
        CustomButton {
            width: 300
            height: 45
            btnText: "退出登录"
            bgColor: "#F44336"
            anchors.top: parent.top
            anchors.topMargin: 30
            anchors.horizontalCenter: parent.horizontalCenter

            onBtnClicked: {
                // 返回到登录页(清空栈,保留登录页)
                stackView.clear()
                stackView.push("qrc:/screens/LoginScreen.qml")
            }
        }
    }
}

4. 方案二:SwipeView 滑动切换(适合平级界面)

SwipeView 支持左右滑动切换界面,适合平级场景(如首页、发现页、我的页),每个界面是独立的 "页签"。

实现示例:主界面使用 SwipeView

css 复制代码
// main.qml
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14
import "./screens"

Window {
    width: 400
    height: 600
    visible: true
    title: "SwipeView 滑动切换"

    SwipeView {
        id: swipeView
        anchors.fill: parent

        // 三个平级界面
        HomeScreen {}
        DiscoverScreen {}
        ProfileScreen {}
    }

    // 底部指示器
    PageIndicator {
        id: indicator
        count: swipeView.count
        currentIndex: swipeView.currentIndex
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        spacing: 10
        padding: 10
    }
}

每个子界面(如 HomeScreen.qml)只需实现自身内容,无需关心切换逻辑:

css 复制代码
// screens/HomeScreen.qml
import QtQuick 2.14
import QtQuick.Controls 2.14

Item {
    width: parent.width
    height: parent.height
    background: Rectangle { color: "#f5f5f5" }

    Text {
        text: "首页"
        font.pointSize: 20
        anchors.centerIn: parent
    }
}

三、C++ 业务逻辑触发 QML 弹窗:跨语言交互实现

在实际项目中,业务逻辑(如网络请求、数据处理、条件判断)通常放在 C++ 中,QML 负责界面展示。当 C++ 逻辑满足某一条件时(如网络异常、数据加载完成),需要触发 QML 弹窗提示。Qt 中实现 C++ 与 QML 交互的核心是 "信号槽机制" 和 "上下文属性暴露"。

1. 交互原理

  • C++ → QML:C++ 类继承 QObject,定义信号;在 QML 中监听该信号,触发弹窗;
  • QML → C++:C++ 类定义 Q_INVOKABLE 修饰的方法,QML 中通过上下文属性调用该方法;
  • 上下文属性:通过 QQmlContext::setContextProperty 将 C++ 对象暴露给 QML,QML 可直接访问该对象的信号、槽和属性。

2. 实战步骤:C++ 触发 QML 弹窗

  • 步骤 1:创建 C++ 业务逻辑类
    创建 BusinessLogic.h 和 BusinessLogic.cpp 文件,实现业务逻辑和信号定义:
cpp 复制代码
// BusinessLogic.h
#ifndef BUSINESSLOGIC_H
#define BUSINESSLOGIC_H

#include <QObject>
#include <QTimer>
#include <QDebug>

class BusinessLogic : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool isNetworkConnected READ isNetworkConnected NOTIFY networkStatusChanged) // 暴露属性给 QML

public:
    explicit BusinessLogic(QObject *parent = nullptr) : QObject(parent) {
        // 模拟网络状态检测(每5秒检测一次)
        m_networkTimer = new QTimer(this);
        m_networkTimer->setInterval(5000);
        connect(m_networkTimer, &QTimer::timeout, this, &BusinessLogic::checkNetworkStatus);
        m_networkTimer->start();
    }

    bool isNetworkConnected() const {
        return m_isConnected;
    }

signals:
    // 网络异常信号(携带错误信息)
    void networkError(const QString &errorMsg);
    // 数据加载完成信号(携带数据)
    void dataLoaded(const QString &data);
    // 网络状态变化信号
    void networkStatusChanged();

public slots:
    // QML 可调用的方法:手动检测网络
    void manualCheckNetwork() {
        checkNetworkStatus();
    }

private slots:
    // 模拟网络检测逻辑
    void checkNetworkStatus() {
        static int count = 0;
        count++;
        if (count % 3 == 0) { // 每3次检测模拟一次网络异常
            m_isConnected = false;
            emit networkError("网络连接失败,请检查网络设置!");
        } else {
            m_isConnected = true;
            emit dataLoaded(QString("最新数据:%1").arg(count));
        }
        emit networkStatusChanged(); // 通知 QML 属性变化
    }

private:
    QTimer *m_networkTimer;
    bool m_isConnected = true;
};

#endif // BUSINESSLOGIC_H
cpp 复制代码
// BusinessLogic.cpp
#include "BusinessLogic.h"

// 无需额外实现,构造函数和槽函数已在头文件中定义
  • 步骤 2:在 main.cpp 中暴露 C++ 对象给 QML
cpp 复制代码
// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "BusinessLogic.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    // 创建业务逻辑对象
    BusinessLogic businessLogic;

    // 将对象暴露给 QML,上下文名称为 "businessLogic"
    engine.rootContext()->setContextProperty("businessLogic", &businessLogic);

    // 加载主 QML 文件
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}
  • 步骤 3:QML 中监听 C++ 信号并实现弹窗
    在 QML 中创建自定义弹窗组件 components/Notification.qml,并在主界面中监听 C++ 信号:
css 复制代码
// components/Notification.qml
import QtQuick 2.14
import QtQuick.Controls 2.14

Rectangle {
    id: notification
    width: 300
    height: 80
    radius: 8
    color: "#333"
    opacity: 0.9
    visible: false // 默认隐藏

    // 弹窗内容
    Column {
        anchors.fill: parent
        anchors.margins: 10
        spacing: 5

        Text {
            id: titleText
            text: "提示"
            color: "white"
            font.pointSize: 16
            font.bold: true
        }

        Text {
            id: contentText
            text: "默认提示内容"
            color: "#eee"
            font.pointSize: 12
        }
    }

    // 关闭按钮
    Button {
        text: "关闭"
        color: "white"
        background: Rectangle {
            color: "#555"
            radius: 4
        }
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.margins: 10
        onClicked: {
            notification.visible = false
        }
    }

    // 显示动画
    Behavior on visible {
        NumberAnimation {
            property: "opacity"
            from: 0
            to: 0.9
            duration: 300
        }
    }

    // 自动关闭(3秒后)
    Timer {
        interval: 3000
        running: notification.visible
        onTriggered: {
            notification.visible = false
        }
    }
}

在 main.qml 中引入弹窗并监听 C++ 信号:

css 复制代码
// main.qml
import QtQuick 2.14
import QtQuick.Window 2.14
import "./components"
import "./screens"

Window {
    width: 400
    height: 600
    visible: true
    title: "C++ 触发 QML 弹窗"

    // 业务逻辑状态显示
    Text {
        id: statusText
        text: "网络状态:" + (businessLogic.isNetworkConnected ? "在线" : "离线")
        font.pointSize: 14
        color: businessLogic.isNetworkConnected ? "#4CAF50" : "#F44336"
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.margins: 10
    }

    // 手动检测网络按钮
    CustomButton {
        x: 200
        y: 10
        btnText: "手动检测网络"
        bgColor: "#FF9800"
        width: 150
        height: 35
        onBtnClicked: {
            businessLogic.manualCheckNetwork() // 调用 C++ 方法
        }
    }

    // 数据显示区域
    Text {
        id: dataText
        text: "等待数据加载..."
        font.pointSize: 14
        color: "#333"
        anchors.top: statusText.bottom
        anchors.left: parent.left
        anchors.margins: 10
    }

    // 自定义弹窗(居中显示)
    Notification {
        id: notification
        anchors.centerIn: parent
    }

    // 监听 C++ 网络异常信号
    Connections {
        target: businessLogic
        onNetworkError: {
            notification.titleText.text = "网络异常"
            notification.contentText.text = errorMsg
            notification.visible = true
        }
    }

    // 监听 C++ 数据加载完成信号
    Connections {
        target: businessLogic
        onDataLoaded: {
            dataText.text = data
        }
    }

    // 监听网络状态变化(更新 UI)
    Connections {
        target: businessLogic
        onNetworkStatusChanged: {
            statusText.text = "网络状态:" + (businessLogic.isNetworkConnected ? "在线" : "离线")
            statusText.color = businessLogic.isNetworkConnected ? "#4CAF50" : "#F44336"
        }
    }
}

3. 交互注意事项

  • 类型匹配:C++ 信号 / 槽的参数类型需与 QML 兼容(如 QString 对应 QML 的 string,QVariant 对应 QML 的 var);
  • 线程安全:若 C++ 信号从非 UI 线程发出,需确保信号处理逻辑线程安全(可通过 Qt::QueuedConnection 连接);
  • 上下文生命周期:暴露给 QML 的 C++ 对象需确保生命周期长于 QML 引擎,避免野指针;
  • 属性更新:C++ 中修改暴露给 QML 的属性后,需发射 propertyChanged 信号(如 networkStatusChanged),QML 才会更新 UI。

四、实践总结与进阶技巧

1. 模块化开发最佳实践

  • 组件粒度:组件不宜过大(如一个完整的登录页可拆分为 LoginForm.qml、LoginButton.qml),也不宜过小(如一个简单的文本标签无需封装);
  • 属性命名规范:暴露的属性使用 camelCase 命名(如 btnText),与 QML 原生属性风格一致;
  • 默认值设置:为组件属性设置合理的默认值,减少外部配置成本;
  • 样式统一:通过 Theme.qml 封装全局样式(如颜色、字体),组件中引用全局样式,确保界面风格一致。

2. 屏幕切换优化

  • 预加载与懒加载:对于复杂界面,可使用 Loader 组件实现懒加载(仅在切换到时加载),提升启动速度;
  • 切换动画:通过 StackView 的 pushTransition 和 popTransition 自定义切换动画(如淡入淡出、缩放);
  • 状态保存:对于需要保存状态的界面(如表单输入),可在 Component.onDestruction 时将状态保存到 C++ 或 Settings 中,下次进入时恢复。

3. C++ 与 QML 交互进阶

  • 数据模型共享:使用 QAbstractListModel 实现 C++ 数据模型,QML 中通过 ListView 直接绑定,支持数据动态更新;
  • 回调函数传递:QML 中通过 function 类型的属性将回调函数传递给 C++,C++ 中通过 QJSValue 调用该函数;
  • 枚举类型暴露:C++ 中使用 Q_ENUMS 或 Q_ENUM 声明枚举,通过 qmlRegisterUncreatableType 注册到 QML,QML 可直接使用枚举值。

五、总结

本文介绍了 QML 界面开发的三大核心技术:模块化封装、多文件调用与屏幕切换、C++ 业务逻辑触发 QML 弹窗。通过自定义组件实现代码复用,使用 StackView/SwipeView 管理界面导航,借助信号槽机制实现跨语言交互,可显著提升 QML 项目的可维护性、复用性和扩展性。

在实际开发中,需结合项目需求选择合适的技术方案(如栈式切换适合多级界面,滑动切换适合平级界面),同时还要遵循模块化、组件化的设计思想,将界面与业务逻辑分离,打造高效、稳定、易扩展的 QML 应用。

相关推荐
mldlds3 小时前
使用 Qt 插件和 SQLCipher 实现 SQLite 数据库加密与解密
数据库·qt·sqlite
jf加菲猫6 小时前
第10章 数据处理
xml·开发语言·数据库·c++·qt·ui
森G7 小时前
30、QStandardItemModel 和 QTableView---------Model/View模型视图
c++·qt
sycmancia7 小时前
C++——Qt中的消息处理
开发语言·qt
Lhan.zzZ7 小时前
Qt多线程数据库操作:安全分离连接,彻底解决段错误
数据库·c++·qt·安全
淼淼7638 小时前
QT仪表盘
开发语言·qt
森G11 小时前
29、QStringListModel 和 QListView---------Model/View模型视图
c++·qt
AIminminHu1 天前
OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(5)番外篇:给 CAD 加上“控制台”——让用户能实时“调参数、看性能”)
qt·mfc·cad
楚Y6同学1 天前
QT C++ 实现图像查看器
开发语言·c++·qt·图像查看