使用Qt中的模型视图框架

本篇文章让你能够在阅读完之后,掌握Qt的模型视图框架的大致使用方法。

问题引入

在我们开发较小的软件的时候,我们可能不会注意到模型视图框架的作用。

因为我们的同一份的数据可能只会在同一个窗口中显示,不会存在数据在一个窗口中更新,然后另一个窗口没更新的情况。可能也存在两个窗口之间同一份数据的统一显示问题,由于比较简单,我们可能就通过自定义信号和槽来通知这种更新的方式来同步了两个窗口的数据。

但是,一旦软件的功能变得复杂起来,你可能就会遇到这样的问题:同一份数据,可能需要在软件中7,8个地方以不同的方式呈现给用户,并且还会给不同的视图配备不同的操作手段。这时候,我们就不可能在某个视图发生变化的时候,手动通知其他视图相应地发生变化,那太麻烦了,每多一个视图,原来的视图就需要通知这个新的视图,这违背了设计模式中的“开闭原则”。

Model-View-Controller架构就能够很好地处理这个问题。

View本身不应该包含业务逻辑,Model需要有对数据进行CRUD的能力,而Controller接收用户的输入,调用模型处理数据,然后选择视图来显示结果。

Qt中参考了这个MVC架构,搞出来一个Model/View + Delegate的模型视图架构。这里Delegate能够对单个数据记录的编辑和显示做出定义,它可以让用户直观地使用简单Widget来修改某部分数据,并且控制单份数据在View中的显示方式。

你可能会疑惑为什么Controller没有了?我猜测这是因为Qt中的信号与槽机制取代了一部分Controller功能的缘故。


若我们想要使用Qt开发数据交互复杂的软件,那么你绝对需要使用到模型视图框架。在本篇文章中,我想要讨论以下问题:

  1. 如何为自己所开发软件的业务数据定义Model。

  2. 如何通过自定义Delegate来自定义数据Item的绘制方式。

  3. View和Delegate提供的编辑器无法对复杂的数据Item进行编辑操作,我们应该如何解决这个问题。

自定义Model

使用Model的方法

Qt中有实现几个Model,但都不太可能直接运用到自己写的软件中。这里我举一个使用Model的例子,来看看Model是如何使用的:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QSplitter* splitter = new QSplitter(Qt::Horizontal);
​
    QFileSystemModel* model = new QFileSystemModel;
    model->setRootPath(QDir::currentPath());
​
    QTreeView* tree = new QTreeView(splitter);
    tree->setModel(model);
    tree->setRootIndex(model->index(QDir::currentPath()));
​
    QListView* list = new QListView(splitter);
    list->setModel(model);
    list->setRootIndex(model->index(QDir::currentPath()));
​
    splitter->show();
    return a.exec();
}

效果如下:

这是Qt文档所给的例子,我们以Tree和List的视图来展示同一份数据。如何显示是后面需要关心的事情,如何实现自己的Model,将其交给视图进行呈现?

我们观察上面例子可以发现,视图设置Model的方法为:

tree->setModel(model);
list->setModel(model);

这里的setModel()​接收一个QAbstractItemModel*​。我们可以通过子类化QAbstractItemModel​来实现自定义的Model。

但通常来说,我们不会直接子类化QAbstractItemModel​,如果我们的Model中的数据是列表,那么可以子类化QAbstractListModel​;如果Model中的数据是Table,那么可以子类化QAbstractTableModel​会更加方便(指重写的工作量更小)。

若Model中的数据是树形的,那么实现可能比较复杂,首先你需要将你的数据(比如一个结构体),用QStandardItem​封装起来,因为单纯的数据可能并不存在树形结构,而QStandardItem​让你重写的方法实际上就提供了这样的树形结构;接着子类化QStandardItemModel​。

简单数据的自定义Model

这里我以一个简单的例子来说明如何自定义Model:

// StringListModel.h
class StringListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    StringListModel(const QStringList& list, QObject* parent = nullptr)
        : QAbstractListModel(parent)
        , m_list(list)
    {
    }
​
    int rowCount(const QModelIndex &parent = QModelIndex()) const
    {
        Q_UNUSED(parent); // 树形结构才会使用到parent
        return m_list.size();
    }
​
    QVariant data(const QModelIndex &index, int role) const
    {
        if (!index.isValid() || index.row() >= rowCount() || index.row() < 0)
            return {};
        switch(role) {
        case Qt::DisplayRole: { return m_list[index.row()]; }
        default: {return {};}
        }
        return {};
    }
​
private:
    QStringList m_list;
};
​
// main.cpp
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QSplitter* splitter = new QSplitter(Qt::Horizontal);
​
    QStringList str_list;
    str_list << "One" << "Two" << "Three" << "Four";
​
    StringListModel* model = new StringListModel(str_list);
​
    QTreeView* tree = new QTreeView(splitter);
    tree->setModel(model);
​
    QListView* list = new QListView(splitter);
    list->setModel(model);
​
    splitter->show();
    return a.exec();
}

需要重写的方法只有两个rowCount()​和data()​。将其接入View中,显示效果如下:

这里解释一下data()​的role​参数的作用:role​实际上就是为了避免定义太多重复的接口而定义的。为什么这么说呢?比如,View​可能需要使用到model->data()​来获取一个叫DisplayText()​的数据,它可能需要调用model->data_DisplayText()​来获取。以此类推还有EditText()​,ToolTipText()​...将近十多个接口,这样实现Model的人就会很头疼。于是他们抽象出来一个Qt::ItemDataRole​,需要什么数据就自己往data(role)​中传role来获取。Qt在文档中定义了标准的Qt::ItemDataRole​所需要返回的数据。

这里我们再返回一个Role数据,你会发现视图相应地会在某些场合下显示这些数据,比如Qt::ToolTipRole​会在鼠标长久停留在某个数据Item不动时显示:

        switch(role) {
        case Qt::DisplayRole: { return m_list[index.row()]; }
        case Qt::ToolTipRole: { return tr("ToolTip"); }
        default: {return {};}
        }

复杂数据类型的自定义Model

只有Qt::ItemDataRole​中定义的Role会被View所使用。Qt::ItemDataRole​告诉View需要什么类型的数据,可以利用Qt::ItemDataRole​中的某个值到Model的data方法中取。但实际上它并没有解决一个问题:“开发者如何从data()​方法中取数据呢?”。我们假设Model中的数据变得复杂起来,它变成了:

struct Record
{
    int id,
    QString name
};

或许我们可以使用Qt::DisplayRole​获取到name​,但是Qt::ItemDataRole​实际上并没有规定可以返回int​的Role。而我们也不能随意修改Qt::ItemDataRole​应该返回的数据的类型,比如Qt::DisplayRole​本应返回QString​类型的数据,但你修改成了int​那么视图显示就会出现问题。

那么这种情况下,我们应该怎么办呢?Qt::ItemDataRole​中有一个枚举非常有趣:

  • Qt::UserRole = 0x100​,应用程序以特定目的使用的使用的第一个Role。

这里面包含两层信息,Qt::UserRole​我们可以随意定义其返回值的类型;“第一个”表示它后面的所有值我们都可以进行自定义,这也就是data()​中的role是int类型而非Qt::ItemDataRole​的原因,你可以自定义一个枚举来从data()​中获取Record中的值:

enum RecordDataRole
{
    Id = 0x101,
    Name,
}

下面我们完成这样一个程序:点击某个数据Item的时候,使用QMessageBox显示该Record的id和name:

// RecordListModel.h
struct Record
{
​
    enum RecordDataRole
    {
        Id = 0x101,
        Name,
    };
​
    int id;
    QString name;
​
    // 用来在Model中获取成员数据
    QVariant data(int role) const
    {
        switch (role) {
        case Id: { return id; }
        case Name: { return name; }
        default: { return {}; }
        }
        return {};
    }
};
​
class RecordListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    RecordListModel(const QVector<Record>& recordList, QObject *parent = nullptr)
        : QAbstractListModel{parent}
        , m_record_list(recordList)
    {
    }
​
    int rowCount(const QModelIndex &parent = QModelIndex()) const
    {
        Q_UNUSED(parent);
        return m_record_list.size();
    }
​
    QVariant data(const QModelIndex &index, int role) const
    {
        if (!index.isValid() || index.row() >= rowCount() || index.row() < 0)
            return {};
        switch (role) {
        case Qt::DisplayRole: { return m_record_list[index.row()].name; }
        default: { return m_record_list[index.row()].data(role); }
        }
        return {};
    }
​
private:
    QVector<Record> m_record_list;
};
​
// main.cpp
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QSplitter* splitter = new QSplitter(Qt::Horizontal);
​
    QVector<Record> record_list;
    record_list.push_back({.id = 1, .name = "Student1 say: Hello World!"});
    record_list.push_back({.id = 2, .name = "Student2 say: Hello Qt!"});
    record_list.push_back({.id = 3, .name = "Student3 say: Hello C++!"});
    record_list.push_back({.id = 4, .name = "Student4 say: Hello MVC!"});
​
    RecordListModel* model = new RecordListModel(record_list);
​
    QListView* list = new QListView(splitter);
    list->setModel(model);
​
    auto show_record = [list](const QModelIndex& index) {
        // 这里是关键,使用index来访问model中对应的数据。
        int id = index.data(Record::Id).toInt();
        QString name = index.data(Record::Name).toString();
        QMessageBox::information(list, "Record", QString("id=%1,name=%2").arg(id).arg(name));
    };
    QObject::connect(list, &QListView::doubleClicked, list, show_record);
​
    splitter->show();
    return a.exec();
}

在双击某个数据Item之后,界面的效果如下:

QModelIndex

你可以发现,我们在访问Model内部数据时并没有使用到model​,而是直接使用了index.data()​。之前并没有介绍QModelIndex​这个东西,这里遇到了就讲一下。你可能会在编写树形Model才需要真正了解其中的一些含义:

QModelIndex中有三个关键的变量:row​,column​和internalPointer​。

  • 我们知道的是QModelIndex指向一个数据,而internalPointer​就是指向这个数据的变量。但若是仅仅指向数据,那是不足以形成树形结构的。这些数据中肯定还包含了一些变量,比如指向parent的指针和所有子节点的指针。

由图来表示可能比较易于理解:

注意,普通的QModelIndex是有可能因为Model发生改变而失效的。比如我们可能往列表头插入新数据,那么原来的index的row就不对了,但是该QModelIndex又还能用。因此QModelIndex最好是拿到即用,而不要用变量保存下来下次再用,因为你不知道下次使用该index是否还是指向原来的数据。

编辑Model中的数据

如果我想要在某个View中编辑Model中的数据,然后将这个编辑反馈到引用该Model的所有视图中。Qt的模型视图框架如何做到这一点?每当修改完成,将修改的范围通过dataChanged()​信号发送给所有View,View依据这个范围看看自己需不需要进行更新。原理就那么简单。

我们不能够通过index.model()->setData()​的方式来修改数据,因为index.model()​是const方法,不能够调用model->setData()​这个非const方法。

因此,想要修改Model,你必须拿到Model。这里我们完成一个小程序,每当我们双击一次Item,Record.id​就加1,然后更新视图:

初始的效果是这样,对某个Item双击之后,就会更新Model中的Id,最终发送dataChanged()​使得所有视图做出反馈:

最终的效果就是:

看起来有点糊,但是具体的功能确实是达成了。你可以使用以下代码在自己的机器上运行复现:

// RecordListModel.h
struct Record
{
    enum RecordDataRole
    {
        Id = 0x101,
        Name,
    };
​
    int id;
    QString name;
​
    QVariant data(int role) const
    {
        switch (role) {
        case Id: { return id; }
        case Name: { return name; }
        default: { return {}; }
        }
        return {};
    }
​
    bool setData(const QVariant& value, int role)
    {
        switch (role) {
        case Id:{
            int old_id = id;
            int new_id = value.toInt();
            id = new_id; // 这里一般而言会成功,但不成功的话我们依旧是需要回滚数据
            if (id == new_id) {
                return true;
            }
            else {
                id = old_id;
                return false;
            }
        }
        } // end of switch
        return false;
    }
};
​
class RecordListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    RecordListModel(const QVector<Record>& recordList, QObject *parent = nullptr)
        : QAbstractListModel{parent}
        , m_record_list(recordList)
    {
    }
​
    int rowCount(const QModelIndex &parent = QModelIndex()) const
    {
        Q_UNUSED(parent);
        return m_record_list.size();
    }
​
    QVariant data(const QModelIndex &index, int role) const
    {
        if (!index.isValid() || index.row() >= rowCount() || index.row() < 0)
            return {};
        switch (role) {
        case Qt::DisplayRole: { return QString("%1%2").arg(m_record_list[index.row()].name).arg(m_record_list[index.row()].id); }
        default: { return m_record_list[index.row()].data(role); }
        }
        return {};
    }
​
    bool setData(const QModelIndex &index, const QVariant &value, int role)
    {
        if (!index.isValid() || index.row() >= rowCount() || index.row() < 0)
            return false;
        switch(role) {
        default: {
            bool ret = m_record_list[index.row()].setData(value, role);
            // 成功修改发送该信号能让其余视图更新
            // 修改失败时发送该消息能够让做出修改的视图回滚
            emit dataChanged(index, index);
            return ret;
        }
        } // end of switch
        return false;
    }
​
private:
    QVector<Record> m_record_list;
};
​
// main.cpp
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QSplitter* splitter = new QSplitter(Qt::Horizontal);
​
    QVector<Record> record_list;
    record_list.push_back({.id = 1, .name = "Student"});
    record_list.push_back({.id = 2, .name = "Employee"});
    record_list.push_back({.id = 3, .name = "Worker"});
    record_list.push_back({.id = 4, .name = "Leader"});
​
    RecordListModel* model = new RecordListModel(record_list);
​
    QListView* list1 = new QListView(splitter);
    list1->setModel(model);
​
    QListView* list2 = new QListView(splitter);
    list2->setModel(model);
​
    QListView* list3 = new QListView(splitter);
    list3->setModel(model);
​
    auto show_record = [=](const QModelIndex& index) {
        // 每当双击Item,我们就将其id加上某个值,这里设置成+1
        int old_id = index.data(Record::Id).toInt();
        model->setData(index, old_id + 1, Record::Id);
    };
    QObject::connect(list1, &QListView::doubleClicked, show_record);
    QObject::connect(list2, &QListView::doubleClicked, show_record);
    QObject::connect(list3, &QListView::doubleClicked, show_record);
​
    splitter->show();
    return a.exec();
}

接收用户的输入编辑Model

不难发现,上面的编辑逻辑是写死在代码中的,用户无法决定一个Item的ID是多少,只能是按照双击ID+1的方式来进行编辑。

那用户有没有办法双击之后,自己来决定ID的值是多少呢?这就需要使用到Delegate了。准确来说是使用Delegate来获取编辑器(实际上就是一个Widget)。

请注意,如果你想要某个数据Item可以在Delegate中获取编辑器,你需要重写QAbstractItemModel::flags()​,让该数据Item具有编辑Flag:

    Qt::ItemFlags flags(const QModelIndex &index) const
    {
        if (!index.isValid())
            return Qt::NoItemFlags;
        return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
    }

由于默认的Delegate能修改的Role是Qt::ItemDataRole​的数据,自然不可能改到我们自定义的Record::Id​,所以提供自定义的Delegate很有必要。或者说,只要你使用自定义的Model,那就需要做好自定义Delegate的准备。

自定义Delegate的编辑器

按照以下方式子类化QStyledItemDelegate​:

// RecordDelegate.h
class RecordDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    RecordDelegate(QObject* parent = nullptr)
        : QStyledItemDelegate{parent}
    {
    }

    QWidget* createEditor(QWidget *parent,
                          const QStyleOptionViewItem &option,
                          const QModelIndex &index) const override
    {
        Q_UNUSED(option);
        Q_UNUSED(index);
        // 构造editor
        // 这里需要编辑ID,就使用QSpinBox
        QSpinBox* editor = new QSpinBox(parent);
        editor->setFrame(false);
        editor->setMinimum(1);
        editor->setMaximum(INT_MAX);
        return editor;
    }

    void setEditorData(QWidget *editor,
                       const QModelIndex &index) const override
    {
        // 需要向editor中填初始数据
        QSpinBox* spin_box = dynamic_cast<QSpinBox*>(editor);
        if (spin_box)
        {
            spin_box->setValue(index.data(Record::Id).toInt());
        }
    }

    void updateEditorGeometry(QWidget *editor,
                              const QStyleOptionViewItem &option,
                              const QModelIndex &index) const override
    {
        Q_UNUSED(index);
        // 让editor出现在正确的位置
        editor->setGeometry(option.rect);
    }

    void setModelData(QWidget *editor,
                      QAbstractItemModel *model,
                      const QModelIndex &index) const
    {
        // 编辑完成之后向Model写入数据
        QSpinBox* spin_box = dynamic_cast<QSpinBox*>(editor);
        if (!spin_box)
            return;
        int new_id = spin_box->value();
        model->setData(index, new_id, Record::Id);
    }

};

然后,我们在main中这样使用它:

// main.cpp
int main(int argc, char* argv[])
{ 
	// 在上一节的基础上加上
	...
	RecordDelegate* delegate = new RecordDelegate;
    list1->setItemDelegate(delegate);
    list2->setItemDelegate(delegate);
    list3->setItemDelegate(delegate);
	splitter.show();
	return a.exec();
}

并删除图中这段内容:

双击View中的一个Item之后,你就能够以下图的方式编辑ID:

有时候我们使用视图显示数据,是为了方便对该数据进行编辑操作。因此,你可能想要这样使用视图:

QQ_1735662361080

🤓👆,为单个Item设置一个按键然后我们一按这个按键就可以让Model做出反馈。想法很美好,但是现实却是Item的绘制是发生在Delegate的paint()​方法中的,我们只能够在这个paint()中自己绘制按键和数据,然后自己检测用户按下的位置来达成按钮的功能。这意味着如果你想实现上面那种在view的item中嵌入多个按键的效果,你需要自己定义按键的绘制逻辑和事件处理逻辑,这会非常耗费精力。

我这里可以提供一种替代方案,最终的功能相近,但用户体验可能不好,却胜在实现简单且可以重复利用视图。

在大多数情况下Model中的Item数据都是一致的,就可以将这些按钮放在同一个widget中,若某个Item被选中,我们就可以将其信息投射到右边的widget中显示出来,并启动其中的按钮,让用户可以点击按钮操作数据。有的时候editor界面实在没办法在View给的有限空间中绘制也可以使用这样的方法。

这里我完成了一个示例:

代码示例我直接上传到Gitee中了,感兴趣的可以去自行查看:项目首页 - Qt模型视图架构使用示例 - GitCode

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/946668.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

跟着逻辑先生学习FPGA-实战篇第一课 6-1 LED灯闪烁实验

硬件平台&#xff1a;征战Pro开发板 软件平台&#xff1a;Vivado2018.3 仿真软件&#xff1a;Modelsim10.6d 文本编译器&#xff1a;Notepad 征战Pro开发板资料 链接:https://pan.baidu.com/s/1AIcnaGBpNLgFT8GG1yC-cA?pwdx3u8 提取码:x3u8 1 知识背景 LED&#xff0c;又名…

2024国城杯 Web

这四道题目Jasper大佬都做了镜像可以直接拉取进行复现 https://jaspersec.top/2024/12/16/0x12%20%E5%9B%BD%E5%9F%8E%E6%9D%AF2024%20writeup%20with%20docker/ n0ob_un4er 这道题没有复现成功, 不知道为啥上传了文件, 也在 /tmp目录下生成了sess_PHPSESSID的文件, 但是就是…

【ArcGISPro/GeoScenePro】检查多光谱影像的属性并优化其外观

数据 https://arcgis.com/sharing/rest/content/items/535efce0e3a04c8790ed7cc7ea96d02d/data 操作 其他数据 检查影像的属性 熟悉检查您正在使用的栅格属性非常重要。

基于Docker基础与操作实战

6.1 Docker容器简介 Docker是一个开源的应用容器引擎&#xff0c;它基于Go语言并遵从Apache2.0 协议开源。 Docker是一个用于开发&#xff0c;交付和运行应用程序的开放平台。Docker能将应用程序与基础架构分开&#xff0c;从而可以快速交付软件。借助Docker&#xff0c;您可…

使用exe4j将jar转成exe、java打包exe

1、maven打包 需要配置以下插件&#xff0c;注意skip为false 插件配置中设置 <skip>true</skip> 时&#xff0c;实际上是告诉 Maven 在构建过程中跳过 spring-boot-maven-plugin 插件的执行。也就是说&#xff0c;Maven 在打包时不会将项目打包成可执行的 JAR 文…

MySQL 的事务与多版本并发控制(MVCC)的那些事

什么是事务原子性:一致性隔离性 问题1: 为什么MySQL要使用mvcc实现隔离性而不使用 锁 解决并发问题?持久性 问题2: MySQL 不是磁盘数据库吗,持久化为什么是 redo log 保证的?问题 3: redo log 储存了什么东西,持久化(崩溃恢复是怎么做的?)问题 4 : MySQL 的 bing log (二进制…

【单片机通讯协议】—— 常用的UART/I2C/SPI等通讯协议的基本原理与时序分析

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、通信基本知识1.1 MCU的参见外设1.2 通信的分类按基本的类型从传输方向上来分 二、UART&#xff08;串口通讯&#xff09;2.1 简介2.2 时序图分析2.3 UART的…

Ceph 手动部署(CentOS9)

#Ceph手动部署、CentOS9、squid版本、数字版本19.2.0 #部署服务:块、对象、文件 一、部署前规划 1、兼容性确认 2、资源规划 节点类型节点名称操作系统CPU/内存硬盘网络组件安装集群节点CephAdm01CentOS94U/8GOS:40G,OSD:2*100GIP1:192.169.0.9(管理&集群),IP2:…

CentOS7 解决ping:www.baidu.com 未知的名称或服务

CentOS7 解决ping&#xff1a;www.baidu.com“未知的名称或服务 在VM查看网络配置 查看虚拟网络编辑器 编辑网络配置文件 vi /etc/sysconfig/network-scripts/ifcfg-ens33注意&#xff1a;不同机器的配置文件名可能不相同&#xff0c;通过 ip addr 命令查看 将 ONBOOT 从 no 改…

GPU 进阶笔记(二):华为昇腾 910B GPU

大家读完觉得有意义记得关注和点赞&#xff01;&#xff01;&#xff01; 1 术语 1.1 与 NVIDIA 术语对应关系1.2 缩写2 产品与机器 2.1 GPU 产品2.2 训练机器 底座 CPU功耗操作系统2.3 性能3 实探&#xff1a;鲲鹏底座 8*910B GPU 主机 3.1 CPU3.2 网卡和网络3.3 GPU 信息 3.3…

[ICCD 2022]Towards Sparsification of Graph Neural Networks

论文网址&#xff1a;[2209.04766] Towards Sparsification of Graph Neural Networks 论文代码&#xff1a;github.com 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#…

人工智能与传统编程的主要区别是什么?

传统编程&#xff1a;开发者预先编写软件行为规则&#xff0c;代码基于程序员定义逻辑处理输入并产生确定输出&#xff0c;具有确定性、手动编写规则和结构化逻辑特点&#xff0c;如垃圾邮件分类程序基于预设关键词等规则。AI 编程&#xff1a;从数据中学习而非手动编写规则&am…

Mac电脑python多版本环境安装与切换

我当前是python3.9.6环境&#xff0c;需要使用3.9.8环境&#xff0c;通过brew安装3.9.8版本&#xff0c;然后通过pyenv切换环境 步骤 1: 安装 pyenv brew install pyenv brew install pyenv-virtualenv 步骤 2: 安装 Python 3.9.8&#xff08;使用 pyenv 安装指定版本的 Pyth…

UE5通过蓝图节点控制材质参数

通过蓝图节点控制材质的参数 蓝图节点 在材质上设置标量值 和 在材质上设置向量参数值 Set Scalar Parameter Value on Materials Set Vector Parameter Value on Materials 这两个蓝图节点都可以在蓝图中&#xff0c;控制材质的参数值和向量值

人工智能(AI)简史:推动新时代的科技力量

一、人工智能简介 人工智能&#xff08;AI&#xff0c;Artificial Intelligence&#xff09;是计算机科学的一个分支&#xff0c;旨在研究和开发可以模拟、扩展或增强人类智能的系统。它涉及多种技术和方法&#xff0c;包括机器学习、深度学习、自然语言处理&#xff08;NLP&a…

Github优质项目推荐(第十期)

文章目录 Github优质项目推荐&#xff08;第十期&#xff09;一、【postiz-app】&#xff0c;14.6k stars - 您的终极 AI 社交媒体调度工具二、【lobe-chat】&#xff0c;50.1k stars - AI 聊天框架三、【cobalt】&#xff0c;22.1k stars - 媒体下载器四、【build-your-own-x】…

DeepSeek V3“报错家门”:我是ChatGPT

搜 &#xff1a;海讯无双Ai 要说这两天大模型圈的顶流话题&#xff0c;那绝对是非DeepSeek V3莫属了。 不过在网友们纷纷测试之际&#xff0c;有个bug也成了热议的焦点—— 只是少了一个问号&#xff0c;DeepSeek V3竟然称自己是ChatGPT。 甚至让它讲个笑话&#xff0c;生成…

通过 4 种方法将数据从 OnePlus 传输到Android

概括 由于它们是不同的品牌&#xff0c;因此将数据从 OnePlus 传输到Android是否很困难&#xff1f;也许您可以从这篇介绍 OnePlus 到Coolmuster Android数据传输的 4 个实用解决方案的文章中获得帮助。学习完它们后&#xff0c;您将有一个顺利的转移过程&#xff0c;所以为什…

Spring Boot的开发工具(DevTools)模块中的热更新特性导致的问题

问题&#xff1a; java.lang.ClassCastException: class cn.best.scholarflow.framework.system.domain.entity.SysUser cannot be cast to class cn.best.scholarflow.framework.system.domain.entity.SysUser (cn.best.scholarflow.framework.system.domain.…

频域滤波为什么使用psf2otf函数?

MATLAB中circshift函数是psf2otf函数的核心&#xff0c;在MATLAB中circshift函数的原理分析——psf2otf函数的核心直观解释了为什么需要循环移位。 MATLAB提出了psf2otf函数&#xff0c;先做循环移位&#xff0c;再计算离散傅里叶变换。如果有空域的卷积核&#xff0c;通过这个…