【学习笔记】C++ GUI Qt4 项目之SpreadSheet源码合集(Qt5可运行版本)


详细的源码打包下载地址:https://download.csdn.net/download/King_why_love/12518707
也可以关注我的微信公众号: 不止编程,回复 SpreadSheet获取。


SpreadSheet(电子表格程序)主要包含书中第二、三、四章节内容。
1、SpreadSheet是一个Excel操作封装类,使用起来比其他的更为方便。
2、修正了书中SpreadSheet几个错误问题。
3、压缩包里面包含SpreadSheet的源码详细注释,以及编译打包后的文件。
4、使用QtCreator Qt5.6.3版本编译通过。
在这里插入图片描述

1 项目运行结果

主界面

在这里插入图片描述
在这里插入图片描述

2 项目结构

源码目录

源码目录

项目结构图

在这里插入图片描述

3 项目源码

3.1 main.cpp

#include <QApplication>

#include "mainwindow.h"

int main(int argc, char *argv[])
{
   
    QApplication app(argc, argv);
    MainWindow mainWin;
    mainWin.show();
    return app.exec();
}

3.2 MainWindow类

//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class QAction;
class QLabel;
class FindDialog;
class Spreadsheet;

namespace Ui {
   
class MainWindow;
}

class MainWindow : public QMainWindow
{
   //MainWindow类定义为QMainWindow类的子类。
 //由于类MainWindow提供了自己的信号和槽,所以它包含了Q_OBJECT宏
    Q_OBJECT

public:
    MainWindow();
    ~MainWindow();

protected:
    void closeEvent(QCloseEvent *event);
    //closeEvent()函数是QWidget类中的一个虚函数,当用户关闭窗口时,这个函数会被自动调用。
    //类MainWindow中重新实现了它,这样就可以向用户询问一个标准问题“Do you want to save yourchanges?" ,并且可以把用户的一些偏好设置保存到磁盘中。

private slots:
    void newFile();
    void open();
    bool save();
    bool saveAs();
    void find();
    void goToCell();
    void sort();
    void about();
    /*
     * 像File->New和Help->About这样的菜单项,在MainWindow中会被实现为私有槽。
     * 除了save()槽和saveAs( )槽返回一
     * 个bool值以外,绝大多数的槽都把void作为它们的返回值。
     * 当槽作为一个信号的响应函数而被执行时,就会忽略这个返回值;
     * 但是当把槽作为函数来调用时,其返回值对我们的作用就和调用任何一个普通的C++函数时的作用是相同的。
     */
    void openRecentFile();
    void updateStatusBar();
    void spreadsheetModified();

private:
    Ui::MainWindow *ui;
    void createActions();
    void createMenus();
    void createContextMenu();
    void createToolBars();
    void createStatusBar();
    void readSettings();
    void writeSettings();
    bool okToContinue();
    bool loadFile(const QString &fileName);
    bool saveFile(const QString &fileName);
    void setCurrentFile(const QString &fileName);
    void updateRecentFileActions();
    QString strippedName(const QString &fullFileName);

    Spreadsheet *spreadsheet;
    FindDialog *findDialog;
    QLabel *locationLabel;
    QLabel *formulaLabel;
    QStringList recentFiles;
    QString curFile;

    enum {
    MaxRecentFiles = 5 };
    QAction *recentFileActions[MaxRecentFiles];
    QAction *separatorAction;

    QMenu *fileMenu;
    QMenu *editMenu;
    QMenu *selectSubMenu;
    QMenu *toolsMenu;
    QMenu *optionsMenu;
    QMenu *helpMenu;
    QToolBar *fileToolBar;
    QToolBar *editToolBar;
    QAction *newAction;
    QAction *openAction;
    QAction *saveAction;
    QAction *saveAsAction;
    QAction *exitAction;
    QAction *cutAction;
    QAction *copyAction;
    QAction *pasteAction;
    QAction *deleteAction;
    QAction *selectRowAction;
    QAction *selectColumnAction;
    QAction *selectAllAction;
    QAction *findAction;
    QAction *goToCellAction;
    QAction *recalculateAction;
    QAction *sortAction;
    QAction *showGridAction;
    QAction *autoRecalcAction;
    QAction *aboutAction;
    QAction *aboutQtAction;
};

#endif // MAINWINDOW_H
//mainwindow.cpp
#include <QtWidgets>//包含了在子类中所要用到的所有Qt类的定义

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "finddialog.h"
#include "gotocelldialog.h"
#include "sortdialog.h"
#include "spreadsheet.h"


MainWindow::MainWindow():ui(new Ui::MainWindow)
{
   
    ui->setupUi(this);

    spreadsheet = new Spreadsheet;
    setCentralWidget(spreadsheet);
    //创建一个Spreadsheet窗口,并设置为中央窗口部件
    //Spreadsheet类是QTableWidget类的一个子类,并且也具有一些电子制表软件的功能

    createActions();
    createMenus();
    createContextMenu();
    createToolBars();
    createStatusBar();
    //创建主窗口中的其余部分

    readSettings();
    //读取这个应用程序存储的一些设置

    findDialog = 0;
    //我们把findDialog指针初始化为空(null)指针。在第一次调用MainWindow::find()函数时,将会创建该FindDialog对象。

    setWindowIcon(QIcon(":/images/icon.png"));
    //把窗口图标设置为icon.png
    //资源机制
    setCurrentFile("");
}

MainWindow::~MainWindow()
{
   
    delete ui;
}

void MainWindow::closeEvent(QCloseEvent *event)
{
   
    /* 当用户单击File->Exit或者单击窗口标题栏中的关闭按钮时,将会调用QWidget::close()槽。
     * 该槽会给这个窗口部件发射一个”close“事件。通过重新实现QWidget::closeEvent()函数就可以中途截取对这个主窗口的关闭操作,并且可以确定到底是不是真的要关闭这个窗口。
     * 如果存在未保存的更改并且用户选择了Cancel, 就会“忽略”这个关闭事件并且让这个窗口不受该操作的影响。
     * 一般情况下,我们会接受这个事件,这会让Qt隐藏该窗口。
     * 也可以调用私有函数writeSettings()来保存这个应用程序的当前设置。
     * 当最后一个窗口关闭后,这个应用程序就结束了。
     * 如果需要,通过把QApplication的quitOnLastWindowClosed属性设置为false,可以禁用这种行为。
     * 在这种情况下,该应用程序将会持续保持运行,直到调用QApplication:: quit()函数,程序才会结束。
     */
    if (okToContinue()) {
   
        writeSettings();
        event->accept();
    } else {
   
        event->ignore();
    }
}

void MainWindow::newFile()
{
   
    /* 当用户点击File->New菜单项或者单击工具栏上的New按钮时,就会调用newFile()槽。
     * 如果存在还没有被保存的信息, okToContinue( )私有函数就会弹出对话框:“Do you wantto save your changes?"。
     * 如果用户选择Yes或者No(保存文档应该选择Yes),这个函数会返回true;
     * 如果用户选择Cancel,它就返回false。 Spreadsheet::clear()函数会清空电子制表软件中的全部单元格和公式。
     * setCurentFile()私有函数会更新窗口的标题,以说明正在编辑的是一个没有标题的文档,它还会设置curFile私有变量并且更新最近打开文件的列表。
     */
    if (okToContinue()) {
   
        spreadsheet->clear();
        setCurrentFile("");
    }
}

void MainWindow::open()
{
   
    /* open()槽对File->Open做出响应。就像newFile()一样,它首先调用okToContiniue()函数来处理任何没有被保存的变化。
     * 然后它使用方便的QFileDialog::getOpenFileName()静态函数从用户那里获得一个新的文件名。
     * 这个函数会弹出一个文件对话框,让用户选择一个文件,并且返回这个文件名——或者,如果用户单击了Cancel按钮,则返回一个空字符串。
     */
    if (okToContinue()) {
   
        /* 传递给QFileDialog::getOpenFileName()函数的第一个参数是它的父窗口部件。
         * 用于对话框和其他窗口部件的这种父子对象关系意义并不相同。
         * 对话框通常都拥有自主权,但是如果它有父对象,那么在默认情况下,它就会居中放到父对象上。
         * 一个子对话框也会共用它的父对象的任务栏。
         * 第二个参数是这个对话框应当使用的标题。
         * 第三个参数告诉它应当从哪一级目录开始,在这个例子中就是当前目录。
         * 第四个参数指定了文件过滤器。文件过滤器(filter)由一个描述文本和一个通配符组成。
         * 如果除了要支持Spreadsheet本地文件格式以外,还需要支持采用逗号分隔的数据文件和Lotus 1-2-3 文件,就应当使用如下的文件过滤器:
         * tr( "Spreadsheet files (* . sp)\n"
         * "Comma-separated values files (* . csv)\n"
         * "Lotus 1-2-3 files (* .wk1 * . wks)")
         */
        QString fileName = QFileDialog::getOpenFileName(this,
                                   tr("Open Spreadsheet"), ".",
                                   tr("Spreadsheet files (*.sp)"));
        if (!fileName.isEmpty())
            //loadFile()私有函数是在open()中得到调用的,它用来载入文件。
            //我们让它成为一个独立的函数,是因为会在载入最近打开的文件中使用同样的功能。
            loadFile(fileName);
    }
}

bool MainWindow::save()
{
   
    /* save()槽对File->Save做出响应。
     * 如果因为这个文件是之前打开的文件或者它是一个已经保存过的文件,这样已经有了一个名字,
     * 那么save()函数就会用这个名字调用saveFile()函数;
     * 否则,它只是简单地调用saveAs()函数。
     */
    if (curFile.isEmpty()) {
   
        return saveAs();
    } else {
   
        return saveFile(curFile);
    }
}

bool MainWindow::saveAs()
{
   
    /* saveAs()槽对File->Save As做出响应。
     * 调用QFileDialog::getSaveFileName()函数来从用户那里得到一个文件名。
     * 如果用户单击了Cancel,则返回false,这将会使这个结果向上传递给它的调用者[save()或者okToContinue()]。
     * 如果给定的文件已经存在, geSaveFileName( )函数将会要求用户确认是否需要覆盖该文件。
     * 但通过给getSveFileNarne() 函数传递一个QFileDialog::DontConfirmOverwite附加参数,则可以改变这一行为。
     */
    QString fileName = QFileDialog::getSaveFileName(this,
                               tr("Save Spreadsheet"), ".",
                               tr("Spreadsheet files (*.sp)"));
    if (fileName.isEmpty())
        return false;

    return saveFile(fileName);
}

void MainWindow::find()
{
   
    //创建非模态对话框时,通常会把它的信号连接到能够对用户的交互做出响应的那些槽上。
    if (!findDialog) {
   
        findDialog = new FindDialog(this);
        connect(findDialog, SIGNAL(findNext(const QString &,
                                            Qt::CaseSensitivity)),
                spreadsheet, SLOT(findNext(const QString &,
                                           Qt::CaseSensitivity)));
        connect(findDialog, SIGNAL(findPrevious(const QString &,
                                                Qt::CaseSensitivity)),
                spreadsheet, SLOT(findPrevious(const QString &,
                                               Qt::CaseSensitivity)));
    }
    /* Find对话框是一个可以让用户在电子制表软件中搜索文本的窗口。当用户单击Edit->Find时,就会调用find( )槽来弹出Find对话框。
     * 这时,就可能出现下列几种情形:
     * ●这是用户第一次调用Find对话框。
     * ●以前曾经调用过Find对话框,但用户关闭了它。.
     * ●以前曾经调用过Find对话框,并且现在它还是可见的。
     * 如果Find对话框还不曾存在过,就可以创建它并且把它的findNext()信号和findPrevious()信号与Spreadsheet中相对应的那些槽连接起来。
     * 本应该在MainWndow的构造函数中创建这个对话框,但是推迟对话框的创建过程将可以使程序的启动更加快速。
     * 还有,如果从来没有使用到这个对话框,那么它就决不会被创建,这样可以既节省时间又节省内存。
     * 然后,调用show()、raise()和activateWindow()来确保窗口位于其他窗口之上并且是可见的和激活的。
     * 只调用show()就足以让一个隐藏窗口变为可见的、位于最上方并且是激活的,
     * 但是也有可能是在Find对话框窗口已经是可见的时候又再次调用了它,在这种情况下,show()调用可能什么也不做,
     * 那么就必须调用raise()和activateWindow()让窗口成为顶层窗口和激活状态。
     */
    findDialog->show();
    findDialog->raise();
    findDialog->activateWindow();
}

void MainWindow::goToCell()
{
   
    /* 如果对话框被接受,函数QDialog::exec()可返回一个true值(QDialog::Accepted),否则就会返回一个false值(QDialog::Rected)。
     * 可以回想一下,当初在第2章利用Qt设计师创建Go to Cell对话框时,就曾经把OK连接到accept(),把Cancel连接到reject()。
     * 如果用户选择0K,就把当前单元格的值设置成行编辑器中的值。
     */
    GoToCellDialog dialog(this);
    if (dialog.exec()) {
   
        QString str = dialog.getLineEdit();
        spreadsheet->setCurrentCell(str.mid(1).toInt() - 1,
                                    str[0].unicode() - 'A');
    }
    /* QTable::setCurentCell()函数需要两个参数:一个行索引和一个列索引。
     * 在Spreadsheet 应用程序中,单元格A1就是单元格(0,0),单元格B27就是单元格(26,1)。
     * 为了从函数QLineEdit::text()返回的QString中获得行索引,可以使用QString::mid()来提取行号(这个函数将返回一个从字符串的开始直到末尾位置的子字符串),
     * 然后使用QString::toInt()把它转换成一个整数值,并且把该值再减去1。
     * 对于列号,则可以用这个字符串中第一个字符的大写数值减去字符‘A'的数值而得到。
     * 我们知道,该字符串将具有正确的格式,因为为对话框创建了一个QRegExpValidator检验器,只有满足一个字符后面再跟至多三位数字格式的字符串才能让OK按钮起作用。
     * goToCell()函数与目前着到的所有代码都有些不同,因为它在堆栈中创建了一个作为变量的窗口部件(一个GoToCellDialog)。
     * 虽然多使用了一行代码,但是换来了不需要使用new和delete的简便。
     * 由于在使用完一个对话框(或者菜单)后,通常就不再需要它了,所以在堆栈中创建对话框(和上下文菜单)是一种常见的编程模式,并且对话框会在作用域结束后自动销毁掉。
     */
}

void MainWindow::sort()
{
   
    /* sort()函数中的代码使用了一种和goToCell()函数中用到的类似模式:
     * ● 在堆栈中创建对话框并且对其进行初始化。
     * ● 使用exec()弹出对话框。
     * ● 如果用户单击0K,就从对话框的各个窗口部件中提取并且使用这些用户输入的值。
     * setColumnRange()调用将那些可用于排序的列变量设置为选定的列。
     * 例如,使用图3.14中的选择,range.leftColumn()将返回值0,即‘A'+0=‘A’,并且range.rightColumn()将返回值2,即‘A' +2=‘C'。
     * compare对象储存了主键、第二键和第三键以及它们的排序顺序。(将会在下一章中看到SpreadsheetCompare类的定义)
     * 这个对象会由Spreadsheet::sort()使用,用于两行的比较。
     * keys数组存储了这些键的列号。
     * 例如,如果选择区域是从C2扩展到E5,那么列C的位置就是0。
     * ascending数组中按bool格式存储了和每一个键相关的顺序。
     * QComboBox::currentIndex()返回当前选定项的索引值,该值是一个从0开始的数。
     * 对于第二键和第三键,考虑到“None”项,我们从当前项减去1。
     */
    SortDialog dialog(this);
    QTableWidgetSelectionRange range = spreadsheet->selectedRange();
    dialog.setColumnRange('A' + range.leftColumn(),
                          'A' + range.rightColumn());

    if (dialog.exec()) {
   
        SpreadsheetCompare compare;
        compare.keys[0] =
              dialog.getPrimaryColumnComboIndex();
        compare.keys[1] =
              dialog.getSecondaryColumnComboIndex() - 1;
        compare.keys[2] =
              dialog.getTertiaryColumnComboIndex() - 1;
        compare.ascending[0] =
              (dialog.getPrimaryOrderComboIndex() == 0);
        compare.ascending[1] =
              (dialog.getSecondaryOrderComboIndex() == 0);
        compare.ascending[2] =
              (dialog.getTertiaryOrderComboIndex() == 0);
        spreadsheet->sort(compare);
    }
}

void MainWindow::about()
{
   
    //通过调用一个方便的静态函数QMessageBox::about(),就可以获得About对话框。
    //这个函数和QMessageBox::warming()的形式非常相似,只是它使用了父窗口的图标,而不是标准的“瞥告”图标。
    QMessageBox::about(this, tr("About Spreadsheet"),
            tr("<h2>Spreadsheet 1.1</h2>"
               "<p>Copyright &copy; 2008 Software Inc."
               "<p>Spreadsheet is a small application that "
               "demonstrates QAction, QMainWindow, QMenuBar, "
               "QStatusBar, QTableWidget, QToolBar, and many other "
               "Qt classes."));
}

void MainWindow::openRecentFile()
{
   
    /* 当用户选择了一个最近打开的文件,就会调用openRecentFile()槽。
     * 只要有任何未保存的变化,就会调用okToContinue()函数,并且假定用户没有取消,还可以使用QObject::sender()查出是哪个特有动作调用了这个槽。
     * qobject_cast <T>()函数可在Qt的moc(meta-object comipiler, 元对象编译器)所生成的元信息基础上执行动态类型强制转换(dynamic cast)。
     * 它返回一个指向所需QObject子类的指针,或者是在该对象不能被转换成所需的那种类型时返回0。
     * 与标准C++的dynamic_ cast <T> ()不同,Qt的qobject_ cast <T> ()可正确地跨越动态库边界。
     * 在例子中,使用qobject _cast <T> ()把一个QObject指针转换成QAction指针。
     * 如果这个转换是成功的(应当是这样的),就可以利用从动作的data项中所提取的文件全名来调用loadFile()函数。
     * 顺便值得一提的是,由于知道这个发射器是一个QAction,如果使用static_cast <T> ()或者传统的C风格的数据类型强制转换代替原有的数据转换方式,这个程序应当仍然是可以运行的。
     * 请参见附录D中“类型转换”一节对不同C++数据类型强制转换的概述。
     */
    if (okToContinue()) {
   
        QAction *action = qobject_cast<QAction *>(sender());
        if (action)
            loadFile(action->data().toString());
    }
}

void MainWindow::updateStatusBar()
{
   
    //updatestatusBar()槽可以更新单元格定位指示器和单元格公式指示器。
    //只要用户把单元格光标移动到一个新的单元格,这个槽就会得到调用。
    locationLabel->setText(spreadsheet->currentLocation());
    formulaLabel->setText(spreadsheet->currentFormula());
}

void MainWindow::spreadsheetModified()
{
   
    //spreadsheeModified()槽把windowModified属性设置为true,用以更新标题栏。
    //这个函数也会更新位置和公式指示器,以便可以让它们反映事件的当前状态。
    setWindowModified(true);
    updateStatusBar();
}

void MainWindow::createActions()
{
   
    newAction = new QAction(tr("&New"), this);
    newAction->setIcon(QIcon(":/images/new.png"));
    newAction->setShortcut(QKeySequence::New);
    newAction->setStatusTip(tr("Create a new spreadsheet file"));
    connect(newAction, SIGNAL(triggered()), this, SLOT(newFile()));
    /* 动作New有一个加速键(&New即Alt+N)、一个父对象(主窗口)、一个图标、一个快捷键和一个状态提示。
     * 大多数窗口系统都有用于特定动作的标准化的键盘快捷键。
     * 例如,在Windows、KDE和GNOME中,这个New动作就有一个快捷键Ctrl+N,而在MacOSX中则是Command+N。
     * 通过使用适当的QKeySequence::StandardKey枚举值,就可以确保Qt能够为应用程序在其运行的平台上提供正确的快捷键。
     * 把这个动作的tiggered()信号连接到主窗口的私有槽newFile()——将会在下一节实现它。
     * 这个连接可以确保在用户选择File New菜单项、选择工具栏上的New按钮或者按下Ctrl+N时,都可以调用newFile()槽。
     * 由于菜单中的Open、Save和SaveAs动作与New动作非常相似,
     * 所以将会直接跳到File菜单中的“recently opened files"(最近打开的文件)的部分。
     */

    openAction = new QAction(tr("&Open..."), this);
    openAction->setIcon(QIcon(":/images/open.png"));
    openAction->setShortcut(QKeySequence::Open);
    openAction->setStatusTip(tr("Open an existing spreadsheet file"));
    connect(openAction, SIGNAL(triggered()), this, SLOT(open()));

    saveAction = new QAction(tr("&Save"), this);
    saveAction->setIcon(QIcon(":/images/save.png"));
    saveAction->setShortcut(QKeySequence::Save);
    saveAction->setStatusTip(tr("Save the spreadsheet to disk"));
    connect(saveAction, SIGNAL(triggered()), this, SLOT(save()));

    saveAsAction = new QAction(tr("Save &As..."), this);
    saveAsAction->setStatusTip(tr("Save the spreadsheet under a new "
                                  "name"));
    connect(saveAsAction, SIGNAL(triggered()), this, SLOT(saveAs()));

    for (int i = 0; i < MaxRecentFiles; ++i) {
   
        recentFileActions[i] = new QAction(this);
        recentFileActions[i]->setVisible(false);
        connect(recentFileActions[i], SIGNAL(triggered()),
                this, SLOT(openRecentFile()));
    }
    /* 我们为recentFileActions数组添加动作。每个动作都是隐式的,并且会被连接到openRecentFile()槽。
     * 稍后,将会看到如何让这些最新文件中的动作变得可见并且可用。
     */

    exitAction = new QAction(tr("E&xit"), this);
    exitAction->setShortcut(tr("Ctrl+Q"));
    exitAction->setStatusTip(tr("Exit the application"));
    connect(exitAction, SIGNAL(triggered()), this, SLOT(close()));
    /* 这个Exit动作与目前为止所看到的那些动作稍微有些不同。由于没有用于终止应用程序的标准化键序列,所以需要在这里明确指定键序列。
     * 另外一个不同之处是:我们连接的是窗口的close()槽,而它是由Qt提供的。
     */

    cutAction = new QAction(tr("Cu&t"), this);
    cutAction->setIcon(QIcon(":/images/cut.png"));
    cutAction->setShortcut(QKeySequence::Cut);
    cutAction->setStatusTip(tr("Cut the current selection's contents "
                               "to the clipboard"));
    connect(cutAction, SIGNAL(triggered()), spreadsheet, SLOT(cut()));

    copyAction = new QAction(tr("&Copy"), this);
    copyAction->setIcon(QIcon(":/images/copy.png"));
    copyAction->setShortcut(QKeySequence::Copy);
    copyAction->setStatusTip(tr("Copy the current selection's contents "
                                "to the clipboard"));
    connect(copyAction, SIGNAL(triggered()), spreadsheet, SLOT(copy()));

    pasteAction = new QAction(tr("&Paste"), this);
    pasteAction->setIcon(QIcon(":/images/paste.png"));
    pasteAction->setShortcut(QKeySequence::Paste);
    pasteAction->setStatusTip(tr("Paste the clipboard's contents into "
                                 "the current selection"));
    connect(pasteAction, SIGNAL(triggered()),
            spreadsheet, SLOT(paste()));

    deleteAction = new QAction(tr("&Delete"), this);
    deleteAction->setShortcut(QKeySequence::Delete);
    deleteAction->setStatusTip(tr("Delete the current selection's "
                                  "contents"));
    connect(deleteAction, SIGNAL(triggered()),
            spreadsheet, SLOT(del()));

    selectRowAction = new QAction(tr("&Row"), this);
    selectRowAction->setStatusTip(tr("Select all the cells in the "
                                     "current row"));
    connect(selectRowAction, SIGNAL(triggered()),
            spreadsheet, SLOT(selectCurrentRow()));

    selectColumnAction = new QAction(tr("&Column"), this);
    selectColumnAction->setStatusTip(tr("Select all the cells in the "
                                        "current column"));
    connect(selectColumnAction, SIGNAL(triggered()),
            spreadsheet, SLOT(selectCurrentColumn()));

    selectAllAction = new QAction(tr("&All"), this);
    selectAllAction->setShortcut(QKeySequence::SelectAll);
    selectAllAction->setStatusTip(tr("Select all the cells in the "
                                     "spreadsheet"));
    connect(selectAllAction, SIGNAL(triggered()),
            spreadsheet, SLOT(selectAll()));
    //由于槽seletAll()是由QTableWidget的父类之一的QAbstractItemView提供的,所以就没有必要再去亲自实现它。

    findAction = new QAction(tr("&Find..."), this);
    findAction->setIcon(QIcon(":/images/find.png")
  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值