主窗口(101):【例】Application Example [官翻]

Application Example

img

应用程序示例演示了如何实现带有菜单、工具栏和状态栏的标准GUI应用程序。

该示例本身是一个围绕QPlainTextEdit构建的简单文本编辑器程序。

Screenshot of the Application example

应用程序示例的几乎所有代码都在MainWindow类中,该类继承QMainWindow。QMainWindow为具有菜单、工具栏、停靠窗口和状态栏的窗口提供了框架。应用程序在菜单栏中提供文件、编辑和帮助条目,并提供以下弹出菜单:

主窗口底部的状态栏显示光标下菜单项或工具栏按钮的说明。

为了简化示例,最近打开的文件不会显示在“File”菜单中,尽管90%的应用程序都需要此功能。此外,此示例一次只能加载一个文件。SDI和MDI示例演示了如何解除这些限制以及如何实现最近打开的文件处理。

QAction 介绍

QAction是表示一个用户操作的对象,例如保存文件或调用对话框。操作可以放在QMenu或QToolBar中,或者两者都放,或者放在任何其他重新实现QWidget::actionEvent() 的小部件中。

一个动作有一个显示在菜单中的文本、一个图标、一个快捷键、一个工具提示、一个状态提示(显示在状态栏中)、一个“这是什么?”文本等。每当用户调用该操作时(例如,通过单击关联的菜单项或工具栏按钮),它都会发出一个triggered() 信号。

QAction的实例可以通过传递父QObject或使用QMenu、QMenuBar或QToolBar的便利函数之一来创建。我们在菜单中以及窗口上作为父对象的工具栏中创建操作,以防止所有权问题。对于只在菜单中的操作,我们使用方便函数QMenu::addAction() ,它允许我们传递文本、图标和目标对象及其slot成员函数。

创建工具栏与创建菜单非常相似。我们在菜单中放置的相同操作可以在工具栏中重用。创建操作后,我们使用QToolBar::addAction() 将其添加到工具栏。

示例代码还包含一个必须解释的习惯用法。对于某些操作,我们指定一个图标作为QAction构造函数的QIcon。我们使用QIcon::fromTheme() 从底层窗口系统获取正确的标准图标。如果由于平台不支持而失败,我们将传递一个文件名作为回退。在这里,文件名以 : 开头,这样的文件名不是普通的文件名,而是可执行文件存储资源中的路径。我们将在回顾application.qrc 作为项目一部分的文件。

只有当QPlainTextEdit包含选定的文本时,“编辑|剪切”和“编辑|复制”动作才可用。我们在默认情况下禁用它们,并将QPlainTextEdit::copyAvailable()信号连接到QAction::setEnabled()插槽,确保在文本编辑器没有选择时禁用这些操作。

在创建帮助菜单之前,我们调用QMenuBar::addSeparator()。这对大多数小部件样式(例如,Windows和macOS样式)没有影响,但对于某些样式,这确保帮助被推到菜单栏的右侧。

main.cpp

#include <QApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>

#include "mainwindow.h"

int main(int argc, char *argv[])
{
    // 用指定的基名称初始化.qrc文件指定的资源。
    // 通常,当资源作为应用程序的一部分构建时,资源会在启动时自动加载。
    // 在某些平台上,对于存储在静态库中的资源,Q_INIT_RESOURCE()宏是必需的。
    Q_INIT_RESOURCE(application);  // 一堆图片资源

#ifdef Q_OS_ANDROID
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

    QApplication app(argc, argv);   // 定义 QApplication

    // 设置此应用程序的组织的名称
    QCoreApplication::setOrganizationName("QtProjectZSH");
    // 设置此应用程序的名称
    QCoreApplication::setApplicationName("Application Example");
    // 设置此应用程序的版本
    QCoreApplication::setApplicationVersion(QT_VERSION_STR);

    QCommandLineParser parser;  // 定义命令行解析器
    parser.setApplicationDescription(QCoreApplication::applicationName());
    parser.addHelpOption();         // -h, --help and -?
    parser.addVersionOption();     // -v / --version
    parser.addPositionalArgument("file", "The file to open.");  // 附加自定义的file参数
    parser.process(app);  // 处理命令行,命令行是从QCoreApplication实例app获得的

    MainWindow mainWin;
    if (!parser.positionalArguments().isEmpty())  // 获取附加参数的列表不为空--file字符串不为空
        mainWin.loadFile(parser.positionalArguments().first());  // 加载获取文件路径
    mainWin.show();
    return app.exec();
}

MainWindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
# include <QMainWindow>

class QSessionManager;
class QPlainTextEdit;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

    void loadFile(const QString &fileName);

protected:
    // 重新实现QWidget::closeEvent(),以检测用户何时尝试关闭窗口,并警告用户未保存的更改。
    void closeEvent(QCloseEvent *event) override;

private slots:
    void newFile();
    void open();
    bool save();
    bool saveAs();
    void about();
    void documentWasModified();
#ifndef QT_NO_SESSIONMANAGER
    void commitData(QSessionManager &);
#endif

private:
    void createActions();
    void createStatusBar();
    void readSettings();
    void writeSettings();
    bool maybeSave();
    bool saveFile(const QString &fileName);
    void setCurrentFile(const QString &fileName);
//    QString strippedName(const QString &fullFileName);

    QPlainTextEdit *textEdit;
    QString curFile;
};
#endif

MainWindow.cpp

/* 我们首先包括<QtWidgets>,一个头文件,其中包含Qt核心、Qt GUI和Qt小部件模块中所有类的定义。
    这使我们不必把每个类都单独包含进去。我们还包括mainwindow.h。
 * 你可能想知道为什么我们不在mainwindow.h中包含<QtWidgets>,然后就不用它了。原因是,包含来
    自另一个头文件的如此大的头文件可能会迅速降低性能。在这里,它不会造成任何伤害,但一般来说,
    只包含另一个头文件中严格必需的头文件仍然是一个好主意。*/

#include <QtWidgets>
#include "mainwindow.h"

/*在构造函数中,首先创建一个QPlainTextEdit小部件作为主窗口(this对象)的子窗口。
   然后调用QMainWindow::setCentralWidget(),告诉您这将是一个小部件,它占据了主窗口的中心区域,位于工具栏和状态栏之间。*/
MainWindow::MainWindow() : textEdit(new QPlainTextEdit)
{
    // QMainWindow::setCentralWidget 将获取小部件指针的所有权,并在适当的时候将其删除。
    setCentralWidget(textEdit); // 设置中心控件

    createActions();       // 创建操作
    createStatusBar();   // 创建状态栏

    readSettings();        // 读取并恢复用户的设置

    // 在QPlainTextEdit的document对象和documentWasModified()槽之间建立一个信号槽连接。
    // 每当用户修改QPlainTextEdit中的文本时,更新标题栏以显示文件已被修改。
    connect(textEdit->document(), &QTextDocument::contentsChanged,
            this, &MainWindow::documentWasModified);  // 关联文档被修改的槽函数

#ifndef QT_NO_SESSIONMANAGER
    // 设置QGUI应用程序是否启用回退会话管理。
    QGuiApplication::setFallbackSessionManagementEnabled(false);
    connect(qApp, &QGuiApplication::commitDataRequest,
            this, &MainWindow::commitData);  // 连接到commitData
#endif

    setCurrentFile(QString());  // 设置当前文件
    // // 窗口是否使用macOS上的统一标题和工具栏外观,不建议使用
    //    setUnifiedTitleAndToolBarOnMac(true);
}


/* 重新实现QWidget::closeEvent(),以检测用户何时尝试关闭窗口,并警告用户未保存的更改 */
void MainWindow::closeEvent(QCloseEvent *event)
{
    if (maybeSave()) {           // 用户希望关闭应用程序
        writeSettings();            // 首选项保存到磁盘
        event->accept();          // 接受close事件
    } else {         // 用户不希望关闭应用程序
        event->ignore();          // 忽略close事件 应用程序将保持运行
    }
}

/* 当用户从菜单中选择File | New时,将调用newFile()槽 */
void MainWindow::newFile()
{
    if (maybeSave()) {                      // 用户希望关闭应用程序
        textEdit->clear();                    // 清除QPlainTextEdit
        setCurrentFile(QString());        // setCurrentFile() 更新窗口标题并清除windowModified标志
    }
}

/* 用户单击File | open时, 将调用open() 槽。*/
void MainWindow::open()
{
    if (maybeSave()) {
        QString fileName = QFileDialog::getOpenFileName(this);  // 弹出一个QFileDialog,要求用户选择一个文件
        if (!fileName.isEmpty())       // 用户选择一个文件(即文件名不是空字符串)
            loadFile(fileName);          // 函数loadFile() 来实际加载该文件
    }
}

/*用户单击File | save时, 将调用save() 槽 */
bool MainWindow::save()
{
    if (curFile.isEmpty()) {      // 用户尚未提供文件名
        return saveAs();           // 调用saveAs()
    } else {                            // 用户尚已提供文件名
        return saveFile(curFile);   // 调用私有函数saveFile() 来实际保存文件
    }
}

/* saveAs() 函数 */
bool MainWindow::saveAs()
{
    QFileDialog dialog(this);
    dialog.setWindowModality(Qt::WindowModal);     // 对话框模态
    dialog.setAcceptMode(QFileDialog::AcceptSave);  // 设置文件保存模态
    if (dialog.exec() != QDialog::Accepted)   // 首先弹出一个QFileDialog,要求用户提供一个名称。
        return false;                                       // 如果用户单击Cancel,则返回的文件名为空,我们什么也不做。
    return saveFile(dialog.selectedFiles().first());  // 否则,调用saveFile() 函数
}

/* 应用程序的About框是使用QMessageBox::About() 静态函数并依赖于它对HTML子集的支持。
 * 围绕文本字符串的tr() 调用标记要翻译的字符串。在所有用户可见的字符串上调用tr()是一个好习惯,
    以防以后决定将应用程序翻译成其他语言。Qt国际化概述更详细地介绍了tr()。*/
void MainWindow::about()
{
    QMessageBox::about(this, tr("About Application"),
                       tr("The <b>Application</b> example demonstrates how to "
                          "write modern GUI applications using Qt, with a menu bar, "
                          "toolbars, and a status bar."));
}

/*  用户编辑而更改QPlainTextEdit中的文本时,都会调用documentWasModified()槽 */
void MainWindow::documentWasModified()
{
    // 调用QWidget::setWindowModified()使标题栏显示文件已被修改。如何做到这一点在每个平台上有所不同。
    setWindowModified(textEdit->document()->isModified());
}

/* 私有函数 createActions() 私有函数创建QActions并填充菜单和两个工具栏。
 * 代码非常重复,因此我们只显示与File | New、File | Open和Help | About Qt对应的操作。*/
void MainWindow::createActions()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));  // 文件菜单
    QToolBar *fileToolBar = addToolBar(tr("File"));             // 添加工具栏菜单

    const QIcon newIcon = QIcon::fromTheme("document-new", QIcon(":/images/new.png"));
    QAction *newAct = new QAction(newIcon, tr("&New"), this);  // 操作-新建
    newAct->setShortcuts(QKeySequence::New);
    newAct->setStatusTip(tr("Create a new file"));
    connect(newAct, &QAction::triggered, this, &MainWindow::newFile);
    fileMenu->addAction(newAct);
    fileToolBar->addAction(newAct);

    const QIcon openIcon = QIcon::fromTheme("document-open", QIcon(":/images/open.png"));
    QAction *openAct = new QAction(openIcon, tr("&Open..."), this);  // 操作-打开
    openAct->setShortcuts(QKeySequence::Open);
    openAct->setStatusTip(tr("Open an existing file"));
    connect(openAct, &QAction::triggered, this, &MainWindow::open);
    fileMenu->addAction(openAct);
    fileToolBar->addAction(openAct);

    const QIcon saveIcon = QIcon::fromTheme("document-save", QIcon(":/images/save.png"));
    QAction *saveAct = new QAction(saveIcon, tr("&Save"), this);    // 操作-保存
    saveAct->setShortcuts(QKeySequence::Save);
    saveAct->setStatusTip(tr("Save the document to disk"));
    connect(saveAct, &QAction::triggered, this, &MainWindow::save);
    fileMenu->addAction(saveAct);
    fileToolBar->addAction(saveAct);

    const QIcon saveAsIcon = QIcon::fromTheme("document-save-as");
    QAction *saveAsAct = fileMenu->addAction(saveAsIcon, tr("Save &As..."), this, &MainWindow::saveAs); // 操作-另存
    saveAsAct->setShortcuts(QKeySequence::SaveAs);
    saveAsAct->setStatusTip(tr("Save the document under a new name"));

    fileMenu->addSeparator();

    const QIcon exitIcon = QIcon::fromTheme("application-exit");
    QAction *exitAct = fileMenu->addAction(exitIcon, tr("E&xit"), this, &QWidget::close);   // 操作-退出
//    exitAct->setShortcuts(QKeySequence::Quit);  // windows没有为Quit设置快捷键
    exitAct->setShortcuts(QKeySequence::listFromString ("Ctrl+Q"));
    exitAct->setStatusTip(tr("Exit the application"));

    QMenu *editMenu = menuBar()->addMenu(tr("&Edit"));        // 添加编辑菜单
    QToolBar *editToolBar = addToolBar(tr("Edit"));                   // 再添加编辑工具栏

#ifndef QT_NO_CLIPBOARD
    const QIcon cutIcon = QIcon::fromTheme("edit-cut", QIcon(":/images/cut.png"));
    QAction *cutAct = new QAction(cutIcon, tr("Cu&t"), this);       // 操作-剪切
    cutAct->setShortcuts(QKeySequence::Cut);
    cutAct->setStatusTip(tr("Cut the current selection's contents to the clipboard"));
    connect(cutAct, &QAction::triggered, textEdit, &QPlainTextEdit::cut);
    editMenu->addAction(cutAct);
    editToolBar->addAction(cutAct);

    const QIcon copyIcon = QIcon::fromTheme("edit-copy", QIcon(":/images/copy.png"));
    QAction *copyAct = new QAction(copyIcon, tr("&Copy"), this);   // 操作-复制
    copyAct->setShortcuts(QKeySequence::Copy);
    copyAct->setStatusTip(tr("Copy the current selection's contents to the clipboard"));
    connect(copyAct, &QAction::triggered, textEdit, &QPlainTextEdit::copy);
    editMenu->addAction(copyAct);
    editToolBar->addAction(copyAct);

    const QIcon pasteIcon = QIcon::fromTheme("edit-paste", QIcon(":/images/paste.png"));
    QAction *pasteAct = new QAction(pasteIcon, tr("&Paste"), this);      // 操作-粘贴
    pasteAct->setShortcuts(QKeySequence::Paste);
    pasteAct->setStatusTip(tr("Paste the clipboard's contents into the current selection"));
    connect(pasteAct, &QAction::triggered, textEdit, &QPlainTextEdit::paste);
    editMenu->addAction(pasteAct);
    editToolBar->addAction(pasteAct);

    menuBar()->addSeparator();

#endif // !QT_NO_CLIPBOARD

    QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));          // 菜单-帮助
    QAction *aboutAct = helpMenu->addAction(tr("&About"), this, &MainWindow::about); // 操作-帮助
    aboutAct->setStatusTip(tr("Show the application's About box"));

    QAction *aboutQtAct = helpMenu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);  // 操作-Qt帮助
    aboutQtAct->setStatusTip(tr("Show the Qt library's About box"));

#ifndef QT_NO_CLIPBOARD
    cutAct->setEnabled(false);            // 禁用 操作-剪切

    copyAct->setEnabled(false);         // 禁用 操作-复制
    connect(textEdit, &QPlainTextEdit::copyAvailable, cutAct, &QAction::setEnabled);
    connect(textEdit, &QPlainTextEdit::copyAvailable, copyAct, &QAction::setEnabled);
#endif // !QT_NO_CLIPBOARD
}

/* QMainWindow::statusBar()返回一个指向主窗口QStatusBar小部件的指针。
 * 与QMainWindow::menuBar()类似,小部件在函数第一次被调用时自动创建。*/
void MainWindow::createStatusBar()
{
    /* 隐藏正常状态指示并以指定的毫秒数(超时)显示给定消息。
        如果timeout为0(默认值),则消息将一直显示,直到clearMessage()槽被调用,
        或者直到再次调用showMessage()槽来更改消息。
     * 请注意,调用showMessage()是为了显示工具提示文本的临时解释,因此传递0的超时时间不足以显示永久消息。*/
    statusBar()->showMessage(tr("Ready"));
}


/* 从构造函数调用readSettings()函数来加载用户的首选项和其他应用程序设置。
 * QSettings类提供了一个高级接口,用于将设置永久存储在磁盘上。在Windows上,它使用(in)著名的Windows注册表;
 * 在macOS上,它使用原生的基于xml的CFPreferences API;在Unix/X11上,它使用文本文件。*/
void MainWindow::readSettings()
{
    /* QSettings构造函数接受标识公司和产品名称的参数。这确保了不同应用程序的设置是分开的。*/
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());

    /* 我们使用QSettings::value()来提取几何设置的值。QSettings::value()的第二个参数是可选的,如果不存在,则为设置
        指定一个默认值。此值在应用程序第一次运行时使用。*/
    const QByteArray geometry = settings.value("geometry", QByteArray()).toByteArray();
    if (geometry.isEmpty()) {  // 如果没有
        const QRect availableGeometry = screen()->availableGeometry();
        resize(availableGeometry.width() / 3, availableGeometry.height() / 2);  // 调整大小
        move((availableGeometry.width() - width()) / 2,                                   //  移动位置
             (availableGeometry.height() - height()) / 2);
    } else {
        // 使用QWidget::saveGeometry()和Widget::restoreGeometry()来保存位置。
        //  它们使用不透明的QByteArray来存储屏幕编号、几何形状和窗口状态。
        restoreGeometry(geometry);           // 恢复设置
    }
}

/* writessettings()函数是从closeEvent()调用的。
 * 写入设置与读取设置类似,只是更简单一些。
 * 构造函数QSettings的参数必须与readSettings()中的参数相同。*/
void MainWindow::writeSettings()
{
    // 计算机\HKEY_USERS\S-1-5-21-1785755811-1963137699-1591789978-500\SOFTWARE\QtProjectZSH\Application Example
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
    settings.setValue("geometry", saveGeometry());
}

/*调用maybeSave()函数来保存挂起的更改。
 * 如果有挂起的更改,它会弹出一个QMessageBox,让用户保存文档。
 * 选项有:QMessageBox::Yes、QMessageBox::No、QMessageBox::Cancel。
 * 使用QMessageBox:: default标志将Yes按钮设置为默认按钮(当用户按回车键时调用的按钮);
 * 使用QMessageBox:: escape标志将Cancel按钮变为escape按钮(用户按Esc时调用的按钮)。
  * maybeSave()函数在所有情况下都会返回true,除非用户单击Cancel或保存文件失败。如果返回值为false,调用者必须检查返回值,
    并停止正在进行的操作
*/
bool MainWindow::maybeSave()
{
    if (!textEdit->document()->isModified())
        return true;
    const QMessageBox::StandardButton ret
            = QMessageBox::warning(this, tr("Application"),
                                   tr("The document has been modified.\n"
                                      "Do you want to save your changes?"),
                                   QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
    switch (ret) {
    case QMessageBox::Save:
        return save();
    case QMessageBox::Cancel:
        return false;
    default:
        break;
    }
    return true;
}

/* 在loadFile()中,我们使用QFile和QTextStream来读取数据。QFile对象提供对存储在文件中的字节的访问。*/
void MainWindow::loadFile(const QString &fileName)

{
    QFile file(fileName);
    /* 我们首先以只读模式打开文件。QFile::Text标志表示该文件是文本文件,不是二进制文件。
        在Unix和macOS上,这没有区别,但在Windows上,它确保了在读取时,“\r\n”行尾序列被转换为“\n”。*/
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        QMessageBox::warning(this, tr("Application"),
                             tr("Cannot read file %1:\n%2.")
                             .arg(QDir::toNativeSeparators(fileName), file.errorString()));
        return;
    }

    /* 如果成功打开文件,则使用QTextStream对象读取数据。QTextStream自动将8位数据转换为Unicode QString
        并支持各种编码。如果没有指定编码,QTextStream假设文件使用系统默认的8位编码(例如,Latin-1;
        参见QTextCodec::codecForLocale()获取详细信息)。*/
    QTextStream in(&file);

    /* 由于对QTextStream::readAll()的调用可能需要一些时间,所以在整个应用程序继续运行时,我们将游标设置
        为Qt::WaitCursor。*/
#ifndef QT_NO_CURSOR
    QGuiApplication::setOverrideCursor(Qt::WaitCursor);
#endif
    textEdit->setPlainText(in.readAll());
#ifndef QT_NO_CURSOR
    QGuiApplication::restoreOverrideCursor();
#endif

    setCurrentFile(fileName);

    // 最后,我们调用私有的setCurrentFile()函数(稍后将介绍这个函数),并在状态栏中显示字符串“File loaded”2秒(2000毫秒)。
    statusBar()->showMessage(tr("File loaded"), 2000);
}


/* 保存文件与加载文件类似。我们使用QSaveFile来确保所有数据都被安全写入,并且如果写入失败,现有的文件不会被损坏。
 * 我们使用QFile::Text标志来确保在Windows上,“\n”被转换为“\r\n”,以符合Windows的约定。*/
bool MainWindow::saveFile(const QString &fileName)
{
    QString errorMessage;

    QGuiApplication::setOverrideCursor(Qt::WaitCursor);
    QSaveFile file(fileName);
    if (file.open(QFile::WriteOnly | QFile::Text)) {
        QTextStream out(&file);
        out << textEdit->toPlainText();
        if (!file.commit()) {
            errorMessage = tr("Cannot write file %1:\n%2.")
                    .arg(QDir::toNativeSeparators(fileName), file.errorString());
        }
    } else {
        errorMessage = tr("Cannot open file %1 for writing:\n%2.")
                .arg(QDir::toNativeSeparators(fileName), file.errorString());
    }
    QGuiApplication::restoreOverrideCursor();

    if (!errorMessage.isEmpty()) {
        QMessageBox::warning(this, tr("Application"), errorMessage);
        return false;
    }

    setCurrentFile(fileName);
    statusBar()->showMessage(tr("File saved"), 2000);
    return true;
}

/* 当加载或保存文件时,或当用户开始编辑新文件时(在这种情况下fileName为空),调用setCurrentFile()函数来
    重置几个变量的状态。我们更新curFile变量,清除QTextDocument::modified标志和相关的QWidget:windowModified标志,
    并更新窗口标题以包含新的文件名(或untitled.txt)。*/
void MainWindow::setCurrentFile(const QString &fileName)
{
    curFile = fileName;
    textEdit->document()->setModified(false);
    setWindowModified(false);

    QString shownName = curFile;
    if (curFile.isEmpty())
        shownName = "untitled.txt";
    setWindowFilePath(shownName);
}

/* 在QWidget::setWindowTitle()调用中,围绕curFile的strippedName()函数调用缩短文件名以排除路径。函数如下:*/
//QString MainWindow::strippedName(const QString &fullFileName)

//{
//    return QFileInfo(fullFileName).fileName();
//}

#ifndef QT_NO_SESSIONMANAGER
void MainWindow::commitData(QSessionManager &manager)
{
    qDebug() << "commitData";
    if (manager.allowsInteraction()) {
        if (!maybeSave())
            manager.cancel();
    } else {
        // Non-interactive: save without asking
        if (textEdit->document()->isModified())
            save();
    }
}
#endif

application.qrc

<!DOCTYPE RCC><RCC version="1.0">
<qresource>
    <file>images/copy.png</file>
    <file>images/cut.png</file>
    <file>images/new.png</file>
    <file>images/open.png</file>
    <file>images/paste.png</file>
    <file>images/save.png</file>
</qresource>
</RCC>

图片资源

copy

cut

new

open

paste

save

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值