Jekyll2023-11-19T04:56:55+00:00http://kevinlq.com/feed.xml鹅卵石鹅卵石的个人博客kevin liC++自动序列化和反序列化在实际软件开发中的应用(二)2023-09-05T00:00:00+00:002023-09-05T00:00:00+00:00http://kevinlq.com/2023/09/05/QtSerializationObj(2)<blockquote>
<p>上一篇文章介绍了自动进行序列化和反序列化的方法,其中也指出了其中存在的问题以及不足,今天这篇文章来详细说下如何处理</p>
</blockquote>
<h2 id="背景">背景</h2>
<p>目前使用自动生成属性的方法支持的数据类型有限,并不是所有的数据类型有支持,不支持的类型生成的键值对的值会是 <code class="language-plaintext highlighter-rouge">null</code>,肯定不是我们想要的,因此需要对这些类型进行自定义扩展。</p>
<p>目前不支持的类型主要有以下几种场景:</p>
<ul>
<li>QRect</li>
<li>QSize</li>
<li>QPoint</li>
<li>QList<T>,QVector<T>,比如: QList<int>,QList<float>,QList<double>……</li>
<li>QPolygon, QPolygonF</li>
<li>QLine,QLineF</li>
<li>QMap, QSet,QHash……</li>
<li>std 标准库数据类型</li>
<li>其它自定义类型</li>
</ul>
<h3 id="原因">原因</h3>
<p>为什么上述类型不支持呢?有两个原因:</p>
<p>第一,上述这些类型无法反向解析,无法进行有效的区分,比如你想把一个<code class="language-plaintext highlighter-rouge">QSize</code> 保存成什么样子?</p>
<ul>
<li>数组形式:[10,10]</li>
<li>字符串:”QSize(10,10)”</li>
</ul>
<p>不管哪种方式,都不是通用的,数组格式在反序列化时无法还原,除非手动进行,字符串方式有冗余字段无法满足三方使用。</p>
<p>第二,其实不怪 <code class="language-plaintext highlighter-rouge">Qt</code>,<code class="language-plaintext highlighter-rouge">JSON</code> 的键值就支持这些类型,[侯捷] 曾说过「源码面前了无秘密」,我们再顺便看下<code class="language-plaintext highlighter-rouge">Qt</code>源码。</p>
<p>某个字段的值是这样获取的,返回的是一个 QVariant 类型,这个类型本身可以支持多种数据类型</p>
<pre><code class="language-C++">QVariant v = object->property(proName);
</code></pre>
<p>将获取的键值插入到 <code class="language-plaintext highlighter-rouge">QJsonObject</code> 当中:</p>
<pre><code class="language-C++">jsObj.insert(proName, QJsonValue::fromVariant(v));
</code></pre>
<p>问题就出在这里,<code class="language-plaintext highlighter-rouge">JSON</code> 对象的值需要一个 <code class="language-plaintext highlighter-rouge">QJsonValue</code> 类型,但是 <code class="language-plaintext highlighter-rouge">QJsonValue</code> 仅仅支持常见的基本数据类型</p>
<pre><code class="language-C++"> QJsonValue(Type = Null);
QJsonValue(bool b);
QJsonValue(double n);
QJsonValue(int n);
QJsonValue(qint64 v);
QJsonValue(const QString &s);
QJsonValue(QLatin1String s);
QJsonValue(const QJsonArray &a);
QJsonValue(const QJsonObject &o);
</code></pre>
<p>基于上述两个原因,我们能够做的只能是把其它类型转换成标准<code class="language-plaintext highlighter-rouge">JSON</code> 支持的类型。</p>
<h2 id="方案">方案</h2>
<p>JSON作为通用的数据格式,一般普遍做法需要针对特殊类型字段单独处理,再反序列化时也需要单独处理,下面以<code class="language-plaintext highlighter-rouge">QPoint、QSize</code> 两种类型为例详细展开说下:</p>
<pre><code class="language-C++"> Q_PROPERTY(QSize testSize READ testSize WRITE setTestSize)
Q_PROPERTY(QPoint testPoint READ testPoint WRITE setTestPoint)
</code></pre>
<p>在进行序列化时,判断该属性类型,然后分别进行处理:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> switch (propertyType)
{
case QMetaType::QSize: return serializeSize(value.toSize());
case QMetaType::QSizeF: return serializeSize(value.toSizeF());
default: throw KException{ QByteArray("Invalid type id ") +
QByteArray(QMetaType::typeName(propertyType))};
}
</code></pre></div></div>
<p>QSize和QSizeF 是类似的,因此需要写一个模板来统一处理:</p>
<pre><code class="language-C++">template <class Size>
static inline QVariant serializeSize(const Size &size)
{
return QVariant(QJsonArray{size.width(), size.height()});
}
</code></pre>
<p><strong>扩展</strong></p>
<p>一般我们能想到的是分别实现对<code class="language-plaintext highlighter-rouge">QSize</code>和<code class="language-plaintext highlighter-rouge">QSizeF</code> 进行处理,这个时候你会写两个函数来处理,进阶后你会写一个模板来处理,那么再次进阶下,怎么处理呢?</p>
<p>来看一个更高级的用法(语法糖,C++17才有的)</p>
<pre><code class="language-C++">QJsonValue serializeSize(const std::variant<QSize, QSizeF> &size) const
{
return
std::visit([](const auto &s) -> QJsonArray {
return {s.width(), s.height()};
}, size);
}
</code></pre>
<blockquote>
<p>上述代码可以参考开源项目: https://github.com/Skycoder42/QtJsonSerializer</p>
</blockquote>
<h2 id="总结">总结</h2>
<p>上述提供了一种方案和思路,按照这个思路继续扩展其它数据类型即可,如果感兴趣可以直接看这个开源项目:https://github.com/Skycoder42/QtJsonSerializer, 不过需要 <code class="language-plaintext highlighter-rouge">Qt5.12</code> 及以上版本才支持哦</p>
<p>如果想自己动手实现,顺便深入学习下 <code class="language-plaintext highlighter-rouge">Qt</code> 元对象系统,那么可以一起参与进来,从零实现一个简易版本的序列化库: https://github.com/kevinlq/KSerialize.</p>
<p>授人以鱼不如授人以渔, 方案和思路有了,关键还是要多动手写起来,如果有问题随时留言交流。</p>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2023年9月5日23:41:31
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>kevin li上一篇文章介绍了自动进行序列化和反序列化的方法,其中也指出了其中存在的问题以及不足,今天这篇文章来详细说下如何处理Qt 对象序列化/反序列化2023-08-05T00:00:00+00:002023-08-05T00:00:00+00:00http://kevinlq.com/2023/08/05/QtSerializationObj<blockquote>
<p>阅读本文大概需要 3 分钟</p>
</blockquote>
<h2 id="背景">背景</h2>
<p>日常开发过程中,避免不了对象序列化和反序列化,如果你使用 <code class="language-plaintext highlighter-rouge">Qt</code> 进行开发,那么有一种方法实现起来非常简单和容易。</p>
<h2 id="实现">实现</h2>
<p>我们知道 <code class="language-plaintext highlighter-rouge">Qt</code>的元对象系统非常强大,基于此属性我们可以实现对象的序列化和反序列化操作。</p>
<p>比如有一个学生类,包含以下几个字段:学号、姓名、性别、出生日期等,定义如下类结构:</p>
<pre><code class="language-C++">class DStudent : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName)
Q_PROPERTY(QString number READ number WRITE setNumber)
Q_PROPERTY(QString sex READ sex WRITE setSex)
Q_PROPERTY(QDateTime birthday READ birthday WRITE setBirthda)
public:
explicit DStudent(QObject *parent = nullptr);
QString name() const;
void setName(const QString &newName);
QString number() const;
void setNumber(const QString &newNumber);
QString sex() const;
void setSex(const QString &newSex);
QDateTime birthday() const;
void setBirthda(const QDateTime &newBirthday);
//...
</code></pre>
<p>需要增加那些字段,只需要在上述属性位置继续追加即可,通过把需要反射的字段定义成属性,我们就可以遍历该类的元对象,进而获取其中的属性信息。</p>
<h3 id="序列化-json">序列化 Json</h3>
<pre><code class="language-C++">QJsonObject DStudent::toJson()
{
QJsonObject jsObj = KJsonHelp::object2Json(this);
return jsObj;
}
bool DStudent::fromJson(const QJsonObject &jsObj)
{
return KJsonHelp::json2Object(jsObj, this);
}
</code></pre>
<p>核心代码封装到工具类中,方便其它地方调用,详细实现如下:</p>
<pre><code class="language-C++">QJsonObject KJsonHelp::object2Json(QObject *object)
{
QJsonObject jsObj;
if(nullptr == object)
{
return jsObj;
}
const QMetaObject *pMetaObj = object->metaObject();
for(int i = 0; i < pMetaObj->propertyCount(); i++)
{
auto proName = pMetaObj->property(i).name();
jsObj.insert(proName, QJsonValue::fromVariant(object->property(proName)));
}
if(jsObj.contains("objectName"))
{
jsObj.remove("objectName");
}
return jsObj;
}
bool KJsonHelp::json2Object(const QJsonObject &jsObj, QObject *object)
{
if (jsObj.isEmpty() || nullptr == object)
{
return false;
}
QStringList list;
const QMetaObject *pMetaObj = object->metaObject();
for(int i = 0; i < pMetaObj->propertyCount(); i++)
{
list << pMetaObj->property(i).name();
}
QStringList jsonKeys = jsObj.keys();
foreach(const QString &proName ,list)
{
if(!jsonKeys.contains(proName) || jsObj.value(proName).isNull())
{
continue;
}
object->setProperty(proName.toLocal8Bit().data(), jsObj.value(proName));
}
return true;
}
</code></pre>
<p>任意一个继承 <code class="language-plaintext highlighter-rouge">QObject</code>的对象都可以获取到它的元对象,接着可以获取到属性个数,然后挨个进行遍历即可。</p>
<p>如果想序列化到其他格式的,比如XML,在上述方法中根据 <code class="language-plaintext highlighter-rouge">XML</code> 规则生成即可,这个不是本文的重点。</p>
<h2 id="一些坑和注意点">一些坑和注意点</h2>
<p>当然了并不是所有的类型都支持这种方式自动生成字段的,一些特殊类型或者自定义的类需要自己特殊去实现。</p>
<p>我们可以在上述学生类中,继续添加新的测试属性字段,来看看输出的结果:</p>
<pre><code class="language-C++"> // custome type
Q_PROPERTY(DScore sScore READ sScore WRITE setSScore)
// test other type
Q_PROPERTY(int testInt READ testInt WRITE setTestInt)
Q_PROPERTY(bool testBool READ testBool WRITE setTestBool)
Q_PROPERTY(double testDouble READ testDouble WRITE setTestDouble)
Q_PROPERTY(char testChar READ testChar WRITE setTestChar)
Q_PROPERTY(QUrl testUrl READ testUrl WRITE setTestUrl)
Q_PROPERTY(QVariant testV READ testV WRITE setTestV)
Q_PROPERTY(QStringList testStringList READ testStringList WRITE setTestStringList)
Q_PROPERTY(QRect testRect READ testRect WRITE setTestRect)
Q_PROPERTY(QSize testSize READ testSize WRITE setTestSize)
Q_PROPERTY(QPoint testPoint READ testPoint WRITE setTestPoint)
Q_PROPERTY(QList<int> testIntList READ testIntList WRITE setTestIntList)
Q_PROPERTY(QList<QString> testListString READ testListString WRITE setTestListString)
</code></pre>
<p>打印输出:</p>
<pre><code class="language-C++"> qRegisterMetaType<DScore>("DScore");
DStudent st;
st.setName(QStringLiteral("法外狂徒张三"));
st.setNumber("123456789");
st.setSex(QStringLiteral("男"));
st.setBirthda(QDateTime::currentDateTime());
// test other type
st.setTestInt(10);
st.setTestBool(true);
st.setTestV(12);
st.setTestDouble(12.121212);
st.setTestChar('k'); //->string
st.setTestUrl(QUrl("http://kevinlq.com/")); // -> string
st.setTestStringList(QStringList() << "stringList1" << "stringList2");
st.setTestRect(QRect(10,10,10,10)); // null
st.setTestSize(QSize(10,10)); // null
st.setTestPoint(QPoint(10, 10)); // null
st.setTestIntList({11, 12}); // null
st.setTestListString({"kevinlq", "devstone"}); // null
DScore score;
score.setName("computer");
score.setNumber("001");
st.setSScore(score); // null
qDebug() << "st:" << st.toJson();
</code></pre>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>st: QJsonObject({"birthday":"2023-08-05T19:33:14.815","name":"法外狂徒张三","number":"123456789","sScore":null,"sex":"男","testBool":true,"testChar":"k","testDouble":12.121212,"testInt":10,"testIntList":null,"testListString":null,"testPoint":null,"testRect":null,"testSize":null,"testStringList":["stringList1","stringList2"],"testUrl":"http://kevinlq.com/","testV":12})
</code></pre></div></div>
<p>可以看到很多字段的值是 <code class="language-plaintext highlighter-rouge">null</code>,出现这种问题表示这个类型目前无法直接自动生成,如果你缺失需要这种结构,那么需要自行在序列化函数中进行特殊处理,比如自定义的类型处理:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QJsonObject DStudent::toJson()
{
QJsonObject jsObj = KJsonHelp::object2Json(this);
jsObj.insert("sScore", m_sScore.toJson());
return jsObj;
}
</code></pre></div></div>
<p>上述对定义的课程类,进行了特殊处理,再次编译后,输出的结果如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"birthday": "2023-08-05T23:27:00.757",
"name": "法外狂徒张三",
"number": "123456789",
"sScore": {
"name": "computer",
"number": "001"
}
}
</code></pre></div></div>
<p>可以看到正常输出了我们需要的类型,其他类型可以照葫芦画瓢。</p>
<h2 id="进阶">进阶</h2>
<p>是不是发现问题了,随着你的类属性字段越来越多,手写这么多字段肯定非常累,能否自动生成这些重复的代码呢,答案是可以的,结合前面的文章,就可以编写一份更加紧凑的代码了,详细见这里 http://kevinlq.com/2023/01/16/generateProperty/</p>
<h2 id="总结">总结</h2>
<p>序列化其实有很应用场景,以下是工作站经常使用的小 case:</p>
<ul>
<li>持久化保存类对象,及保存数据到本地磁盘;</li>
<li>socket 传输数据,需要把json/xml/other类型转为对象,用对象进行业务处理;</li>
<li>数据库操作:从 db 中读取出来的值序列化成对象,方便业务进行处理(使用 ORM 框架例外)</li>
<li>和界面交互,比如界面使用 QML 编写,那么大部分超场景会使用到 JSON。</li>
<li>其他:待补充……</li>
</ul>
<h2 id="参考文档">参考文档</h2>
<ul>
<li><a href="http://kevinlq.com/2023/01/16/generateProperty/">c++自动生成get/set方法</a></li>
</ul>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2023年8月5日23:41:31
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>kevin li阅读本文大概需要 3 分钟Qt/C++ 自动生成get和set方法以及属性2023-01-16T00:00:00+00:002023-01-16T00:00:00+00:00http://kevinlq.com/2023/01/16/generateProperty<blockquote>
<p>阅读本文大概需要 3 分钟</p>
</blockquote>
<h2 id="背景">背景</h2>
<p>日常开发过程中,避免不了写很多大量的成员变量,如果这些变量都定义成 <code class="language-plaintext highlighter-rouge">public</code>,那么完全不用写对应的 <code class="language-plaintext highlighter-rouge">get/set</code> 方法,但是很多时候不能也不允许这样做,当变量/属性很多时手写会变得非常麻烦。</p>
<p>一般在使用 <code class="language-plaintext highlighter-rouge">Qt</code> 开发时还会增加相关的属性改变信号等属性,当变量值改变后信号会触发,这些重复、繁琐的工作希望能进一步一起简化,使得我们可以从繁琐的工作中解放出来。</p>
<p>基于以上两点,我们可以宏定义此过程中的实现,使用时一行代码搞定</p>
<h2 id="实现">实现</h2>
<p>在开始动手前可以考虑下,宏定义如何实现变量名拼接、变量命名如何标准化(驼峰法,当然了其它命名规范也可以,主要做到全文保持一致即可)</p>
<h3 id="基本语法">基本语法</h3>
<pre><code class="language-C++">#xx: 参数格式化, 也就是会把传入的参数名转化后参数名字符串
##name: 连接操作, 会把前后两个入参变为一个字符串
示例如下:
#define Function(arg) #arg
输入:std::string str = Function(hello)
输出: 宏展开后就会变成 std::string str = "hello"
#define Function(arg1, arg2) m_##arg
输入: Function(name)
输出:m_name
</code></pre>
<h3 id="小试牛刀">小试牛刀</h3>
<pre><code class="language-C++">#define AUTO_PROPERTY(type, typeName ,variableName) \
Q_PROPERTY(type variableName READ get##variableName WRITE set##variableName NOTIFY s##variableName##Changed ) \
public: \
inline type get##variableName() const { return m_##typeName##variableName ; } \
inline void set##variableName(const type& value) { \
if (m_##typeName##variableName == value) return; \
m_##typeName##variableName = value; \
emit s##variableName##Changed(value); \
} \
Q_SIGNAL void s##variableName##Changed(const type &value);\
private: \
type m_##typeName##variableName;
</code></pre>
<h3 id="调用">调用</h3>
<pre><code class="language-C++">// TestClass
class TestClass : public QObject
{
Q_OBJECT
AUTO_PROPERTY(QString, s, StuName)
READONLY_PROPERTY(QString, s, StuInfo)
public:
explicit TestClass(QObject *parent = nullptr);
};
// main.cpp
TestClass t;
t.setStuName("admin");
qDebug() << t.getStuName() << t.getStuInfo();
</code></pre>
<p>可以看到,大大简化了代码,整体代码量也少很多,可以把重心放在重要的业务和算法中。</p>
<p>但是,没有任何事情是完美的,虽然这样简化了代码,但是有人会说了,代码可读性等是否会降低?到底会不会呢?还是要因人而异吧,你说呢?</p>
<h2 id="总结">总结</h2>
<p>上述代码是在 <code class="language-plaintext highlighter-rouge">gist.github</code> 上某个版本基础上修改而成的,主要处理了命名规范,详细可以参考下文连接。</p>
<p>参考这个代码,可以实现其它版本的功能,比如只读变量、更改变量的访问权限等等,详细内容可以见 <code class="language-plaintext highlighter-rouge">GitHub</code></p>
<h2 id="参考文档">参考文档</h2>
<ul>
<li><a href="https://juejin.cn/post/6963596491017420808">firecat全宏</a></li>
<li><a href="https://gist.github.com/Rolias/48d453a0490d36090193">gist.github</a></li>
<li></li>
</ul>
<h2 id="全部代码">全部代码</h2>
<p>PropertyHelper.h</p>
<pre><code class="language-C++">#pragma once
#include <QObject>
//See Gist Comment for description, usage, warnings and license information
/*
* 自动生成类的成员变量以及对应的属性, 主要有是三种
1.AUTO_PROPERTY: 包含了该变量的所有属性,读/写/改变信号
2.READONLY_PROPERTY: 仅读,没有写方法和信号
3.READ_PROPERTY: 仅读,但是包含了属性改变信号
*
* type: 变量类型,如 int, QString, std::string
* typeName: 类型名称缩写, QString->s, int->n, bool->b
* variableName: 变量名字
*
* 信号前面加前缀s,表示信号.
*/
#define AUTO_PROPERTY(type, typeName ,variableName) \
Q_PROPERTY(type variableName READ get##variableName WRITE set##variableName NOTIFY s##variableName##Changed ) \
public: \
inline type get##variableName() const { return m_##typeName##variableName ; } \
inline void set##variableName(const type& value) { \
if (m_##typeName##variableName == value) return; \
m_##typeName##variableName = value; \
emit s##variableName##Changed(value); \
} \
Q_SIGNAL void s##variableName##Changed(const type &value);\
private: \
type m_##typeName##variableName;
#define READONLY_PROPERTY(type, typeName, variableName) \
Q_PROPERTY(type variableName READ get##variableName CONSTANT ) \
public: \
inline type get##variableName() const { return m_##typeName##variableName ; } \
private: \
type m_##typeName##variableName;
#define READ_PROPERTY(type, typeName, variableName) \
Q_PROPERTY(type variableName READ variableName NOTIFY s##variableName##Changed) \
public: \
inline type get##variableName() const { return m_##typeName##variableName ; } \
Q_SIGNAL void s##variableName##Changed(const type &value);\
private: \
type m_##typeName##variableName;
</code></pre>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2023年1月16日21:24:01
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>kevin li阅读本文大概需要 3 分钟Qt 编写的应用程序无法使用拖拽问题2022-11-12T00:00:00+00:002022-11-12T00:00:00+00:00http://kevinlq.com/2022/11/12/windows_application_drag_drop_question<blockquote>
<p>阅读本文大概需要 2 分钟</p>
</blockquote>
<h2 id="问题背景">问题背景</h2>
<p>接到产品经理的需求:给我们的程序添加一个打开导入文件的方式,允许用户拖动外部文件直接打开该文件,想了下也不难,没有几行代码,随口答应了下来</p>
<p>实际验证下来,发现有很大的坑</p>
<p>正常情况下,肯定使用没有问题,打开程序后随意拖动任何文件都可以感应到,但是当你的程序被用户以「管理员」方式运行时,此时拖拽功能就歇菜了</p>
<p>无论你怎么操作,拖拽文件事件始终检测不到……</p>
<h2 id="原因">原因</h2>
<p>发现问题后,随即搜索了一圈,发现这个是 <code class="language-plaintext highlighter-rouge">Windows</code> 做了限制。有一个叫做 <code class="language-plaintext highlighter-rouge">UAC</code> 的概念先来解释下</p>
<blockquote>
<p>在很早的 <code class="language-plaintext highlighter-rouge">XP</code> 时代还没有这个概念,从 <code class="language-plaintext highlighter-rouge">vista</code> 开始引入了一种安全机制(UAC User Account Control), 不同管理权限的程序是无法直接交互的,为了防止潜在的特权提升攻击,需要阻止某些交互功能。</p>
</blockquote>
<p>出于安全考虑,<code class="language-plaintext highlighter-rouge">Windows</code> 禁止用户程序和管理员程序之间交互,我们的拖拽是消息,触发者是资源管理器(explorer.exe)默认开机后运行是非管理员权限,这样拖拽就有问题</p>
<p>正常权限程序向具有管理员权限的程序发送消息可能会导致安全问题,被禁用了。</p>
<h2 id="方案">方案</h2>
<p>目前来看没有很好的方案来解决,只能在非管理员权限下使用程序,或者强制使 <code class="language-plaintext highlighter-rouge">explorer.exe</code> 以管理员权限运行,如果你的产品是面向 <code class="language-plaintext highlighter-rouge">C</code> 端的产品,那么这个方案肯定不行</p>
<p>好了,到此为止,可以有底气怼产品了,打扰了,无法实现……</p>
<h2 id="参考文档">参考文档</h2>
<ul>
<li><a href="https://stackoverflow.com/questions/37828617/drag-and-drop-not-working-if-application-lies-in-specific-directory">stackoverflow</a></li>
<li><a href="https://forum.qt.io/topic/131155/qt-applications-cannot-drag-and-drop-files-when-running-under-windows-with-administrator-rights">Qt 论坛</a></li>
<li><a href="https://social.technet.microsoft.com/Forums/en-US/cba0e9b1-25f8-40e5-a888-1435d604f68d/quotrun-as-administratorquot-prevents-drag-and-drop-working?forum=w7itprosecurity">Microsoft</a></li>
<li><a href="https://learn.microsoft.com/en-gb/archive/blogs/patricka/q-why-doesnt-drag-and-drop-work-when-my-application-is-running-elevated-a-mandatory-integrity-control-and-uipi">Microsoft about drag and drop</a></li>
<li><a href="https://learn.microsoft.com/en-us/windows/security/identity-protection/user-account-control/how-user-account-control-works">UAC</a></li>
</ul>
<p><strong>推荐阅读</strong></p>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2022年11月12日22:05:35
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>kevin li阅读本文大概需要 2 分钟C++(Qt) 和 Word 交互总结(二)2022-09-11T00:00:00+00:002022-09-11T00:00:00+00:00http://kevinlq.com/2022/09/11/QtC++Word(%E4%BA%8C)<blockquote>
<p>阅读本文大概需要 6 分钟</p>
</blockquote>
<p>之前有一篇文章介绍过 <a href="http://kevinlq.com/2022/03/14/QtC++-Word-Excel-PDF/"><code class="language-plaintext highlighter-rouge">C++/Qt</code> 操作 <code class="language-plaintext highlighter-rouge">Word</code>的一些方法</a>,虽然能满足一部分使用场景,但是终究是在某些平台上有限制,使用起来还是不方便,所以就有了这边文章</p>
<p>我们知道操作 <code class="language-plaintext highlighter-rouge">Word</code>其实还有一种方法,那就按照 <code class="language-plaintext highlighter-rouge">OOXML</code>规范读写即可,<code class="language-plaintext highlighter-rouge">OOXML</code> 是微软 2007之后推出的一套标准,凡是符合这个标准生成的文档都可以正常打开,遗憾的是这方面 <code class="language-plaintext highlighter-rouge">C++</code> 没有可用的库,一是因为本身 <code class="language-plaintext highlighter-rouge">C++</code>人群少,二是是用 <code class="language-plaintext highlighter-rouge">C++</code> 实现工作量大,所以就只能选择现有成熟的轮子</p>
<p><code class="language-plaintext highlighter-rouge">Python</code>有非常多的开源库可以使用,其中有一个<code class="language-plaintext highlighter-rouge">Python-docx</code>库,完美实现了<code class="language-plaintext highlighter-rouge">Word</code>读写,使用 <code class="language-plaintext highlighter-rouge">C++</code> 调用 <code class="language-plaintext highlighter-rouge">Python</code>是非常方便的,所以可以间接来实现 <code class="language-plaintext highlighter-rouge">Word</code>的交互</p>
<p>支持功能:</p>
<ul>
<li>
<p>支持自定义标题,包括样式、字体、对齐方式、标题级别等;</p>
</li>
<li>
<p>支持插入任意行列表格,表格支持单独设置某个单元格样式,字体、颜色、是否加粗、水平、垂直对齐方式等;</p>
</li>
<li>
<p>支持合并任意单元格;</p>
</li>
<li>
<p>支持插入图片,支持相对路径和绝对路径</p>
</li>
<li>
<p>支持市面上大部分平台,不依赖客户电脑安装的 <code class="language-plaintext highlighter-rouge">WPS</code>和<code class="language-plaintext highlighter-rouge">Word</code></p>
</li>
</ul>
<p>下面看测试导出的效果:</p>
<p><img src="./res/img/blog/Qt-learn/word_result2.png" alt="" /></p>
<h2 id="原理介绍">原理介绍</h2>
<p>我们知道 <code class="language-plaintext highlighter-rouge">C/C++/Qt</code>都是编译型语言,也是是说不能直接从源码运行,而<code class="language-plaintext highlighter-rouge">Python</code>是解释型语言,不需要经过编译成二进制代码可以直接从源码运行,在运行 <code class="language-plaintext highlighter-rouge">Python</code>的时候首先经过 <code class="language-plaintext highlighter-rouge">Python</code> 解释器解释,你可以理解成翻译的意思,解释成字节码,然后在一条一条字节码指令开始执行</p>
<p><code class="language-plaintext highlighter-rouge">Python</code>提供了一些<code class="language-plaintext highlighter-rouge">C</code>库,我们可以在<code class="language-plaintext highlighter-rouge">C/C++</code>程序中包含对应头文件、库文件,进而调用函数方法来实现某个功能</p>
<p>调用 <code class="language-plaintext highlighter-rouge">Python</code>主要流程如下:</p>
<ul>
<li>初始化<code class="language-plaintext highlighter-rouge">Python</code>上下文环境(解释器环境)</li>
<li>导入对应的模块</li>
<li>获取对应函数对象,参数转换,调用函数</li>
<li>解析返回值,结束调用</li>
<li>释放 <code class="language-plaintext highlighter-rouge">python</code>解释器</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">C++</code>根据实际业务生成对应的<code class="language-plaintext highlighter-rouge">JSON</code>字符串,然后调用<code class="language-plaintext highlighter-rouge">Python</code>传递给对应函数,在<code class="language-plaintext highlighter-rouge">Python</code>函数中解析<code class="language-plaintext highlighter-rouge">JSON</code>字符串然后生成<code class="language-plaintext highlighter-rouge">Word</code>内容</p>
<blockquote>
<p><a href="https://github.com/kevinlq/QtPythonDocx">整个脚本实现库以及对应 Example 都已经开源,感兴趣的朋友直接访问即可</a></p>
</blockquote>
<h2 id="环境配置">环境配置</h2>
<p>下载并安装好<code class="language-plaintext highlighter-rouge">Python</code>相关库,确保本地环境没有问题,记得安装好<code class="language-plaintext highlighter-rouge">Python-docx</code>库。拷贝<code class="language-plaintext highlighter-rouge">Python</code>相关依赖库到你的项目目录,不如下面这样</p>
<pre><code class="language-C++">QtPythonDocx
| 3rdparty
│ └─Python310
│ ├─include
│ │ ├─cpython
│ │ └─internal
│ └─libs
├─bin
│ ├─Python310
│ │ ├─DLLs
│ │ └─Lib
| |─script
│ │ wordOperate.py
</code></pre>
<blockquote>
<p>关于一些版本事项、以及中间会遇到那些坑,文末有注意事项统一介绍</p>
</blockquote>
<h2 id="调用-python库">调用 <code class="language-plaintext highlighter-rouge">Python</code>库</h2>
<p>为了做到简洁、通用,我们编写一个脚本调用类,该类和具体的业务无关,只负责传入不同模块、函数、参数调用对应的<code class="language-plaintext highlighter-rouge">Python</code>函数并能够返回对应的结果,这样后续的调用者就使用的时候和使用普通函数没有区别</p>
<p>为了实现这个目的,目前有几个知识点需要解决:</p>
<ul>
<li>由于<code class="language-plaintext highlighter-rouge">Python</code>数据类型和<code class="language-plaintext highlighter-rouge">C++</code>不一样,如果要通用那么就需要进行转换,怎么做到<code class="language-plaintext highlighter-rouge">C++</code>一个参数类型匹配<code class="language-plaintext highlighter-rouge">Python</code>多个类型?</li>
<li>返回值处理,我们的业务函数返回值可能多种多样,怎么兼容?</li>
<li>编码转换,<code class="language-plaintext highlighter-rouge">Python</code>中支持<code class="language-plaintext highlighter-rouge">UTF-8</code>,我们程序处理中数据可能包含多种类型,怎么转换</li>
</ul>
<p>解决了上述问题,基本也就是完成了本次要写的脚本加载类</p>
<h3 id="脚本调用类实现">脚本调用类实现</h3>
<p>首先看下类型问题,其实我们这里需要一个万能类型来作为函数入参,那么有这个类型么?有,如果你的编译器支持 <code class="language-plaintext highlighter-rouge">C++17</code>,那么可以用<code class="language-plaintext highlighter-rouge">std::variant</code></p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">variant</span><span class="o"><</span><span class="kt">int</span><span class="p">,</span> <span class="kt">double</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">></span> <span class="n">inputArg</span>
</code></pre></div></div>
<p>由于作者本人对 <code class="language-plaintext highlighter-rouge">Qt</code>比较熟一点,所以本次程序中使用了大量的<code class="language-plaintext highlighter-rouge">Qt</code>内置数据类型,原理是相通的</p>
<p><code class="language-plaintext highlighter-rouge">KPythonRunScript </code>类的实现,核心函数如下所示</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">bool</span> <span class="nf">callFun</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">funcName</span><span class="p">,</span>
<span class="k">const</span> <span class="n">QVariantList</span> <span class="o">&</span><span class="n">args</span> <span class="o">=</span> <span class="n">QVariantList</span><span class="p">(),</span>
<span class="n">QVariant</span> <span class="o">&</span><span class="n">returnValue</span> <span class="o">=</span> <span class="n">QVariant</span><span class="p">(</span><span class="n">QVariant</span><span class="o">::</span><span class="n">Invalid</span><span class="p">));</span>
</code></pre></div></div>
<ul>
<li><code class="language-plaintext highlighter-rouge">funcName</code>: python 脚本中对应的函数名字</li>
<li><code class="language-plaintext highlighter-rouge">args</code>: 函数入参,根据实际脚本中函数参数个数而定</li>
<li><code class="language-plaintext highlighter-rouge">returnValue</code>: 返回值,如果脚本函数有返回值初始化的时候赋予对应类型</li>
</ul>
<p>实际<code class="language-plaintext highlighter-rouge">Python</code>脚本中函数的入参个数是不确定的,为了兼容多个调用场景,所以采用了数组作为实际的入参,数组每个元素采用<code class="language-plaintext highlighter-rouge">QVariant</code>类型,这样就能根据实际传入的类型来判断,在调用<code class="language-plaintext highlighter-rouge">Python</code>的时候应该转换为什么类型</p>
<p>返回值类型也一样,初始化调用时确定好本次调用的返回值类型,这样在<code class="language-plaintext highlighter-rouge">Python</code>脚本调用完成后才能把返回值转为我们<code class="language-plaintext highlighter-rouge">C++</code>实际的返回值</p>
<p>类型转换:</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">index</span> <span class="o"><</span> <span class="n">args</span><span class="p">.</span><span class="n">size</span><span class="p">();</span> <span class="n">index</span><span class="o">++</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">QVariant</span> <span class="n">arg</span> <span class="o">=</span> <span class="n">args</span><span class="p">[</span><span class="n">index</span><span class="p">];</span>
<span class="k">switch</span> <span class="p">(</span><span class="n">arg</span><span class="p">.</span><span class="n">type</span><span class="p">())</span>
<span class="p">{</span>
<span class="k">case</span> <span class="n">QVariant</span><span class="o">::</span><span class="n">String</span><span class="p">:</span>
<span class="p">{</span>
<span class="n">QByteArray</span> <span class="n">baContent</span> <span class="o">=</span> <span class="n">arg</span><span class="p">.</span><span class="n">toString</span><span class="p">().</span><span class="n">toLocal8Bit</span><span class="p">();</span>
<span class="n">PyTuple_SetItem</span><span class="p">(</span><span class="n">pArgsObj</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">Py_BuildValue</span><span class="p">(</span><span class="s">"s"</span><span class="p">,</span> <span class="n">baContent</span><span class="p">.</span><span class="n">constData</span><span class="p">()));</span>
<span class="p">}</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="n">QVariant</span><span class="o">::</span><span class="n">Int</span><span class="p">:</span> <span class="n">PyTuple_SetItem</span><span class="p">(</span><span class="n">pArgsObj</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">Py_BuildValue</span><span class="p">(</span><span class="s">"i"</span><span class="p">,</span> <span class="n">arg</span><span class="p">.</span><span class="n">toInt</span><span class="p">()));</span> <span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="n">QVariant</span><span class="o">::</span><span class="n">Double</span><span class="p">:</span> <span class="n">PyTuple_SetItem</span><span class="p">(</span><span class="n">pArgsObj</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">Py_BuildValue</span><span class="p">(</span><span class="s">"d"</span><span class="p">,</span> <span class="n">arg</span><span class="p">.</span><span class="n">toDouble</span><span class="p">()));</span> <span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="n">QVariant</span><span class="o">::</span><span class="n">LongLong</span><span class="p">:</span> <span class="n">PyTuple_SetItem</span><span class="p">(</span><span class="n">pArgsObj</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">Py_BuildValue</span><span class="p">(</span><span class="s">"l"</span><span class="p">,</span> <span class="n">arg</span><span class="p">.</span><span class="n">toLongLong</span><span class="p">()));</span> <span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="n">QVariant</span><span class="o">::</span><span class="n">Char</span><span class="p">:</span> <span class="n">PyTuple_SetItem</span><span class="p">(</span><span class="n">pArgsObj</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">Py_BuildValue</span><span class="p">(</span><span class="s">"b"</span><span class="p">,</span> <span class="n">arg</span><span class="p">.</span><span class="n">toChar</span><span class="p">().</span><span class="n">toLatin1</span><span class="p">()));</span> <span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="n">QVariant</span><span class="o">::</span><span class="n">Invalid</span><span class="p">:</span> <span class="n">PyTuple_SetItem</span><span class="p">(</span><span class="n">pArgsObj</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">Py_BuildValue</span><span class="p">(</span><span class="s">"()"</span><span class="p">));</span> <span class="k">break</span><span class="p">;</span>
<span class="nl">default:</span> <span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这里目前适配了上述几种类型,如果后续不满足继续扩展其它类型即可</p>
<p><code class="language-plaintext highlighter-rouge">Python</code>脚本对应的函数</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">generateWord</span><span class="p">(</span><span class="n">strContent</span><span class="p">):</span>
<span class="c1">#...
</span> <span class="k">return</span> <span class="bp">True</span>
</code></pre></div></div>
<h3 id="详细调用">详细调用</h3>
<p>在上述实现的类的基础上,调用其实就变的很简单了,就和我们调用本地某个函数一样,非常轻松</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">KPythonRunScript</span> <span class="o">*</span><span class="n">pRunScript</span> <span class="o">=</span> <span class="n">KPythonRunScript</span><span class="p">::</span><span class="n">instance</span><span class="p">(</span><span class="s">"wordOperate"</span><span class="p">);</span>
<span class="n">QVariant</span> <span class="n">returnValue</span> <span class="o">=</span> <span class="n">true</span><span class="p">;</span>
<span class="n">QVariantList</span> <span class="n">args</span> <span class="o">=</span> <span class="p">{</span><span class="s">""</span><span class="p">};</span>
<span class="nb">bool</span> <span class="n">bResult</span> <span class="o">=</span> <span class="n">pRunScript</span><span class="o">-></span><span class="n">callFun</span><span class="p">(</span><span class="s">"generateWord"</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">returnValue</span><span class="p">);</span>
<span class="n">qDebug</span><span class="p">()</span> <span class="o"><<</span> <span class="s">"run generateWord result:"</span> <span class="o"><<</span> <span class="n">bResult</span> <span class="o"><<</span> <span class="n">returnValue</span><span class="p">;</span>
<span class="k">if</span><span class="p">(</span><span class="err">!</span><span class="n">bResult</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">qWarning</span><span class="p">()</span> <span class="o"><<</span> <span class="s">"write word fail....."</span><span class="p">;</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>可能你注意到程序中使用了单例,为什么使用单例?这是因为单个进程<code class="language-plaintext highlighter-rouge">Python</code>解释器相关内容初始化一次即可,后续随意调用不用再次初始化,实际验证中也证实了,多次初始化会有一些异常问题(虽然每次用完已经释放了,再次初始化还是会有问题)</p>
<p>这样就实现了一个简单的调用过程,具体<code class="language-plaintext highlighter-rouge">Python</code>文件中的内容可以看我开源的工程目录中的内容,其实就是把各种操作<code class="language-plaintext highlighter-rouge">Word</code>方法封装成函数了,扩展了常用的字段</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">QtPythonDocx</span><span class="o">/</span><span class="nb">bin</span><span class="o">/</span><span class="n">script</span><span class="o">/</span><span class="n">wordOperate</span><span class="p">.</span><span class="n">py</span>
</code></pre></div></div>
<h2 id="json格式说明"><code class="language-plaintext highlighter-rouge">JSON</code>格式说明</h2>
<p>由于 <code class="language-plaintext highlighter-rouge">Word</code> 内容较多,调用时兼容很多写入场景,因此目前设计使用 <code class="language-plaintext highlighter-rouge">JSON</code> 格式来交互,基本覆盖大部分使用场景,而且支持各种自定义,完全满足日常使用,下面是各个字段的说明</p>
<h3 id="全局配置">全局配置</h3>
<ul>
<li>savePath: 定义了生成的 <code class="language-plaintext highlighter-rouge">Word</code> 文档路径,确保该路径有写入权限,否则可能会失败</li>
<li>openFile: 导入成功后是否打开该文档</li>
<li>line_spacing: 行间距,默认给 1.5倍</li>
<li>header: 页眉文本,不需要页眉直接给空即可</li>
<li>footer: 页脚文本</li>
<li>content:[] 这里是 <code class="language-plaintext highlighter-rouge">Word</code> 内容部分,采用数组存储,由于数组有有序的,因此严格按照你的内容顺序依次传入即可</li>
<li>fontSize: 全局字体大小</li>
<li>fontName: 全局字体名字,设置后后续每个正文、标题、表格等可以不用设置,全局统一</li>
</ul>
<h3 id="正文">正文</h3>
<p>下面是正文内容部分说明</p>
<ul>
<li>type: 标识是那种类型,0:标题,1: 普通文本,2:图片,3:表格,其它类型后续扩展自定义</li>
<li>text: 如果是文本或者标题给定内容</li>
<li>level: 级别,目前只有标题类型生效</li>
<li>bold: 是否加粗</li>
<li>italic: 是否倾斜</li>
<li>strike: 是否删除线</li>
<li>alignment: 对齐方式,主要有这么几种:left, right,center</li>
<li>color: 对应文本的颜色</li>
<li>height: 行高</li>
</ul>
<h3 id="插入表格">插入表格</h3>
<p>如果是表格,那么有这些扩展字段</p>
<ul>
<li>columns: 列数</li>
<li>rows: 行数</li>
<li>height: 行高,所有行设置一样的行高,也可以自定义每行的行高</li>
<li>mergeCells: 要合并的单元格数组,比如合并 (0,0)和(0,1)单元格,那么内容如下
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"begin": [0,0], "end": [0,1]}
</code></pre></div> </div>
</li>
<li>tableCell: 单元格内容,依次填充每个单元格内容即可,每个单元格内容和普通文本类似,下面是一个示例</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tableCell": [
{"text": "我是第一个单元格,加粗,倾斜,红色", "style": "", "bold": true, "italic": true,"color": "#ff0000","alignment": "center"},
{"text": "00和01合并了,02会覆盖01的值,加粗变红,左对齐", "style": "", "bold": true, "italic": false,"color": "#ff0000","alignment": "left"},
{"text": "03", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
{"text": "04", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
{"text": "05", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
{"text": "06", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
{"text": "07", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"},
{"text": "08", "style": "", "bold": false, "italic": false,"color": "#000000","alignment": "center"}
]
</code></pre></div></div>
<h3 id="插入图片">插入图片</h3>
<p>图片字段和其它文本字段类似,额外添加图片路径属性即可</p>
<ul>
<li>picture: “./test.png”</li>
</ul>
<p>注意图片路径支持相对路径和绝对路径,根据自己实际需要传递即可</p>
<h2 id="总结">总结</h2>
<p>本次通过<code class="language-plaintext highlighter-rouge">Python</code>的方式可以很好的支持很多之前出现的异常问题,足以满足我们遇到的各种业务需要导出生成<code class="language-plaintext highlighter-rouge">Word</code>难题,而且导出速度非常快,实际测试生成 <code class="language-plaintext highlighter-rouge">10</code> 页左右文档耗时不到 <code class="language-plaintext highlighter-rouge">2</code>秒,测试了多台电脑,实际效果都非常理想</p>
<h2 id="注意事项">注意事项</h2>
<ul>
<li><code class="language-plaintext highlighter-rouge">Python</code>版本选择问题,确保你的程序最终要运行的平台,如果要最低要求是<code class="language-plaintext highlighter-rouge">Windows7</code>,那么建议选择 <code class="language-plaintext highlighter-rouge">Python3.8</code>版本即可,如果无所谓那么选择最新稳定版本即可;</li>
<li><code class="language-plaintext highlighter-rouge">Python</code> 注意选择和你程序使用同一个位数,程序编译器使用的是 <code class="language-plaintext highlighter-rouge">64</code> 位,那就下载 <code class="language-plaintext highlighter-rouge">64</code> 位,<code class="language-plaintext highlighter-rouge">32</code>位同理 ;</li>
<li>导出的文档使用 <code class="language-plaintext highlighter-rouge">Micor Office 2007</code>之后的任意版本都能打开,<code class="language-plaintext highlighter-rouge">WPS</code>也能打开,但是在 <code class="language-plaintext highlighter-rouge">2007</code>之前的版本是不支持的。不过目前这种场景较少,如果真的遇到了还是给客户建议升级对应的版本吧</li>
</ul>
<p><strong>推荐阅读</strong></p>
<ul>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484658&idx=1&sn=55af25cd6e608fa9cb1452610928e71b&chksm=e85c0ac2df2b83d453d80f66fcdefca31e998a8ac0ba0226edbd80b437f99ed5184478805d8e&scene=21#wechat_redirect">Qt Creator 源码学习笔记01,初识QTC</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484676&idx=1&sn=c1bd8cbd78d8e098c22353f567cc3620&chksm=e85c0b34df2b822288a7baa016f4a64df47e5e119039836ff686cb2ad3a7e28a5808efc0c13a&scene=21#wechat_redirect">Qt Creator 源码学习笔记02,认识框架结构结构</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484701&idx=1&sn=c69eb1edfb551c41690938423ca2ef7c&chksm=e85c0b2ddf2b823b1179f216e57ca91b9ce4068a0469e8ba062ab3596e9dc51ac05a1572da85&scene=21#wechat_redirect">Qt Creator 源码学习笔记03,大型项目如何管理工程</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484767&idx=1&sn=971c14682095a8a84a8161311400100f&chksm=e85c0b6fdf2b82799db871eafb6d0b2465c75d8020b1f87a1f5825f82edcad2c051b7d6e1c2c&scene=21#wechat_redirect">Qt Creator 源码学习笔记04,多插件实现原理分析</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484822&idx=1&sn=eff40e19952a534fc0d97cab2417ce9e&chksm=e85c0ba6df2b82b017eb568adacd5b407f269cc2c2e7d2c36609fdda76b029bdcd70003fadba&token=193645825&lang=zh_CN#rd">Qt Creator 源码学习笔记 05,菜单栏是怎么实现插件化的?</a></li>
</ul>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2022年9月11日17:08:18
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>
<h2 id="捐赠">捐赠</h2>
<center>
<img src="https://gitee.com/devstone/imageBed/raw/master/code/myCode.png" width="50%" height="50%" />
</center>
<p>如果觉得分享的内容不错,可以请作者喝杯咖啡.</p>kevin li阅读本文大概需要 6 分钟QML 播放/渲染视频方式复盘2022-07-16T00:00:00+00:002022-07-16T00:00:00+00:00http://kevinlq.com/2022/07/16/qml-render%20video<h2 id="背景描述">背景描述</h2>
<p>最近刚好有个需求,需要在 <code class="language-plaintext highlighter-rouge">QML</code> 中播放一些视频,以前在没有怎么用过,最近花了一些时间进行了调研,总结下</p>
<p><strong>PS:</strong></p>
<blockquote>
<p>因为之前软件使用 <code class="language-plaintext highlighter-rouge">QWidget</code> ,后来由于种种原因,推翻之前的结构,进行了全部代码重构,所以以前的视频渲染模块不能用了,需要采用新的方式来实现视频渲染播放</p>
</blockquote>
<h2 id="视频编解码框架">视频编解码框架</h2>
<p>目前有很多开源框架可以实现,而且从网络上也能很方便的搜索到很多相关的 <code class="language-plaintext highlighter-rouge">Demo</code>,比较流行的框架有这些:</p>
<ul>
<li><a href="https://github.com/FFmpeg/FFmpeg">FFmpeg</a></li>
<li><a href="https://github.com/videolan/vlc">vlc</a></li>
<li><a href="https://github.com/wang-bin/QtAV">QtAV</a></li>
<li><a href="https://github.com/vlc-qt/vlc-qt">vlc-qt</a></li>
<li><a href="https://github.com/wang-bin/mdk-sdk">mdk-sdk</a></li>
</ul>
<p>由于老项目中使用的也是 <code class="language-plaintext highlighter-rouge">vlc-qt</code>,因此新架构也就继续选择了它,不要问为什么,问就是可以少踩一些坑</p>
<h2 id="使用">使用</h2>
<p><code class="language-plaintext highlighter-rouge">vlc-qt</code> 这个库其实 5 年多就没有再更新了,不过一般使用过程中也不会有太大的问题,目前使用方式有两种:<code class="language-plaintext highlighter-rouge">动态库</code>,<code class="language-plaintext highlighter-rouge">源码继承</code>,一般直接引入库比较方便</p>
<p>可能有时候动态库提供的相关接口功能不太符合你的需求,或者库的版本和你项目中使用的版本不一致,那么这个时候就需要下载源码进行编译了,下面详细说下编译步骤,其实也很简单</p>
<h3 id="下载-vlc-qt代码">下载 <code class="language-plaintext highlighter-rouge">vlc-qt</code>代码</h3>
<p>使用命令行或者其它方式均可</p>
<pre><code class="language-C++">git clone https://download.videolan.org/vlc/
// 更新子模块代码,如果不是使用命令行,可能需要手动下载其他库
git submodule init
git submodule update
</code></pre>
<p>对于 <code class="language-plaintext highlighter-rouge">vlc-sdk</code>,你可以根据自己需要手动下载某个版本, 我选择的是 <code class="language-plaintext highlighter-rouge">3.0</code>的版本</p>
<p>vlc-sdk 下载地址: https://download.videolan.org/vlc/</p>
<h3 id="使用-cmake构建">使用 <code class="language-plaintext highlighter-rouge">cmake</code>构建</h3>
<p>很多人其实会卡在这一步,因为某些环境变量或者字段赋值不对,导致编译出错,其实只要注意这几个点一般不会有问题</p>
<ul>
<li>DLIBVLC_LIBRARY</li>
<li>DLIBVLCCORE_LIBRARY</li>
<li>DLIBVLC_INCLUDE_DIR</li>
</ul>
<p>一定要确定上述库的位置正确</p>
<p>PS: 很多人会参考网上的博客,把 <code class="language-plaintext highlighter-rouge">vlc</code> 很多库和头文件拷贝到 <code class="language-plaintext highlighter-rouge">Qt</code>对应的路径中,这个完全没有必要,也不知道这中骚操作始作俑者是谁,该拉出来,任何时候都不应该污染 <code class="language-plaintext highlighter-rouge">Qt</code>目录,否则遇到一些问题,你怎么死也想不到</p>
<blockquote>
<p>自己日常用的比较多是 <code class="language-plaintext highlighter-rouge">qmake</code>,因此手动吧源码工程改成了支持 <code class="language-plaintext highlighter-rouge">qmake</code>方式,这样下载下来直接使用 <code class="language-plaintext highlighter-rouge">Qt Creator</code> 打开就能编译使用,特方便一些</p>
</blockquote>
<p><a href="https://gitee.com/devstone/vlc-qt-qml-demo">vlc-qt-qmake地址</a></p>
<h2 id="一些坑">一些坑</h2>
<p>本来认为自己对编译三方库信心满满,结果遇到了一个让我 <code class="language-plaintext highlighter-rouge">n</code> 多天都无法解决的难题,不管采用那种方式,<code class="language-plaintext highlighter-rouge">QML</code> 界面始终无法渲染出视频画面……</p>
<p>真的,文章开始提到的几种库都试了,自己公司和家里的环境都出现一样的诡异现象。因为基本都是采用 <code class="language-plaintext highlighter-rouge">OPenGL</code>来实现渲染的,那么肯定是这里出现了问题,后面的时间基本都是在学习了解 <code class="language-plaintext highlighter-rouge">OpenGL</code> 相关的渲染以及 <code class="language-plaintext highlighter-rouge">Qt</code>相关 <code class="language-plaintext highlighter-rouge">Example</code>学习</p>
<p>最后发现很多官方的 <code class="language-plaintext highlighter-rouge">Demo</code> 运行后都是无法渲染的……</p>
<p>以为是我电脑有问题,又在同事电脑上运行一样的 <code class="language-plaintext highlighter-rouge">Demo</code>,现象和我电脑是一样的,这样就排除了我电脑的问题(其实后来证明了,不能排除哈,当时应该再找一台电脑进行验证的)</p>
<p>中间又尝试咨询了很多大佬,也是没有结果,最后在 <code class="language-plaintext highlighter-rouge">stackoverflow</code> 提问了下,有些人的回复刚好启发了我,我的环境始终是软件上下文,后来通过相关日志输入也证实了这一点</p>
<p>软件上下文渲染即我们前端渲染方式,这种模式下是无法使用 <code class="language-plaintext highlighter-rouge">OpenGL</code> 上下文先关内容的,也就会出现对应指针为空的现象</p>
<pre><code class="language-C++">QOpenGLContext *pContent = QOpenGLContext::currentContext();
</code></pre>
<p>那么问题也就慢慢浮出水面了,那个软件把我电脑环境给改了?想到这个问题,突然意识到上家公司软件安装时会强行设置一些环境变量和注册表</p>
<p>于是打开注册表,搜啊搜,搜啊搜……,终于被我找到了</p>
<p>正是下面这个东东,害我怀疑人生……</p>
<p><a href="https://imgtu.com/i/vwpO4H"><img src="https://s1.ax1x.com/2022/08/15/vwpO4H.png" alt="image" /></a></p>
<p>所以,我们遇到那些很诡异、觉得不应该发生的问题时,可能正是因为某种巧合引起的小问题导致的,那些我们平时不注意或者容易疏忽的小问题,未来的某一天可能引发大问题</p>
<blockquote>
<p>PPS:事后证明,巧合真的有,但是谁也没有想到会有这么巧,验证问题的电脑环境都是同样被某个软件修改了</p>
</blockquote>
<p>很多时候巧合是因为我们的样本不够多导致的,而我犯的毛病就是只是在 3 台电脑验证了都有问题,没有再扩大范围验证,导致该问题卡了好久</p>
<h2 id="题外话">题外话</h2>
<p>使用过程中,发现 <code class="language-plaintext highlighter-rouge">mdk-sdk</code>也非常不错,中间和作者就我遇到的问题来回沟通过几次,帮我解决了一些疑惑,真的很感谢</p>
<p>大家也可以看看这个库,这个库的前身是 <code class="language-plaintext highlighter-rouge">QtAv</code>,作者对其进行了重构,目前做到了 <code class="language-plaintext highlighter-rouge">ABI</code>完全兼容多个编译器,非常容易上手,而且自带了很多 <code class="language-plaintext highlighter-rouge">Demo</code></p>
<h2 id="参考文章">参考文章</h2>
<ul>
<li><a href="https://ffmpeg.org/ffmpeg.html">官方</a></li>
<li><a href="https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html">ruanyifeng</a></li>
</ul>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2022年7月16日
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>
<h2 id="捐赠">捐赠</h2>
<center>
<img src="https://s1.ax1x.com/2022/08/15/vw9gMt.png" width="50%" height="50%" />
</center>
<p>如果觉得分享的内容不错,可以请作者喝杯咖啡.</p>kevin li背景描述FFmpeg 学习笔记2022-03-14T00:00:00+00:002022-03-14T00:00:00+00:00http://kevinlq.com/2022/03/14/Audio-Video<h2 id="下载安装配置环境">下载安装配置环境</h2>
<h3 id="下载">下载</h3>
<p><a href="https://www.ffmpeg.org/download.html">下载地址</a></p>
<p><code class="language-plaintext highlighter-rouge">Windows</code> 有两个版本</p>
<ul>
<li>Windows builds from gyan.dev</li>
<li>Windows builds by BtbN</li>
</ul>
<p><a href="https://imgtu.com/i/vwCVoD"><img src="https://s1.ax1x.com/2022/08/15/vwCVoD.md.png" alt="vwCVoD.md.png" /></a></p>
<h3 id="安装">安装</h3>
<p>直接下载的可执行程序,不需要安装直接解压到某个文件夹,将该路径配置到系统环境变量即可</p>
<h3 id="环境变量配置">环境变量配置</h3>
<hr />
<h2 id="常用命令">常用命令</h2>
<p>命令行参数有很多,基本可以分为下面 5 部分内容</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg {1} {2} -i {3} {4} {5}
</code></pre></div></div>
<ul>
<li>全局参数</li>
<li>输入文件参数</li>
<li>输入文件</li>
<li>输出文件参数</li>
<li>输出文件</li>
</ul>
<h3 id="基本命令示例">基本命令示例</h3>
<ul>
<li>查看支持的容器(类型)
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg -formats
</code></pre></div> </div>
</li>
<li>查看视频编码格式
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg -codecs
</code></pre></div> </div>
</li>
<li>查看视频编码器
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg -encoders
</code></pre></div> </div>
</li>
</ul>
<h2 id="比特率">比特率</h2>
<p>比特率又叫码率,用来表示每秒初始的字节数,单位是 <code class="language-plaintext highlighter-rouge">kbps</code> (注意是小写的 k )</p>
<ul>
<li>K 字节数</li>
<li>b 比特(bit)</li>
<li>p 每(per)</li>
<li>s 秒(secend)</li>
</ul>
<p>比特率又一个粗略的计算公式: 文件大小/时长</p>
<p>比如一个文件大为 12MB,时长为 88秒,那么按照粗略计算公式,比特率大概为:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(12 * 1024 * 8) / 88 = 1117.09 kbps
</code></pre></div></div>
<p>实际上比特率又分为三种:<code class="language-plaintext highlighter-rouge">VBR</code>, <code class="language-plaintext highlighter-rouge">ABR</code>, <code class="language-plaintext highlighter-rouge">CBR</code></p>
<ul>
<li><code class="language-plaintext highlighter-rouge">VBR</code>(Variable Bitrate) 动态比特率,数据压缩时根据音频数据实际特点实时确定采用那种采样率,这种方式压缩质量非常高</li>
<li><code class="language-plaintext highlighter-rouge">ABR</code>(Average Bitrate) 平均比特率,这种方式是 <code class="language-plaintext highlighter-rouge">VBR</code>的一种平均值(或者叫插值参数)</li>
<li><code class="language-plaintext highlighter-rouge">CBR</code>(Constant Bitrate) 常数比特率,文件从头到尾比特率不变化</li>
</ul>
<h2 id="参考文章">参考文章</h2>
<ul>
<li><a href="https://ffmpeg.org/ffmpeg.html">官方</a></li>
<li><a href="https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html">ruanyifeng</a></li>
<li><a href="https://toolstud.io/video/bitrate.php?imagewidth=1920&imageheight=1080&colordepth=24&framerate=60">自动计算码率</a></li>
</ul>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2021年3月10
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>
<h2 id="捐赠">捐赠</h2>
<center>
<img src="https://s1.ax1x.com/2022/08/15/vw9gMt.png" width="50%" height="50%" />
</center>
<p>如果觉得分享的内容不错,可以请作者喝杯咖啡.</p>kevin li下载安装配置环境Python 学习笔记2022-03-14T00:00:00+00:002022-03-14T00:00:00+00:00http://kevinlq.com/2022/03/14/Python-Note<h2 id="简介">简介</h2>
<p><a href="https://www.liaoxuefeng.com/wiki/1016959663602400/1017063826246112">参考地址</a></p>
<h2 id="软件安装">软件安装</h2>
<p>开始安装</p>
<p><img src="/res/img/blog/Python/download.png" alt="download" /></p>
<p>Windows 系统</p>
<p><img src="/res/img/blog/Python/install.png" alt="install" /></p>
<p>注意上图,将Python添加到系统环境变量中</p>
<p>点击 Install Now 开始安装</p>
<p>打开命令行,输入Python</p>
<p><img src="/res/img/blog/Python/install_test.png" alt="install_test" /></p>
<p>出现上图显示内容,则表示安装成功了</p>
<h2 id="基本知识">基本知识</h2>
<h3 id="注释">注释</h3>
<pre><code class="language-C++"># 这是一个注释
print("Hello, World!")
</code></pre>
<p>单引号</p>
<pre><code class="language-C++">#!/usr/bin/python3
'''
这是多行注释,用三个单引号
这是多行注释,用三个单引号
这是多行注释,用三个单引号
'''
print("Hello, World!")
</code></pre>
<p>双引号</p>
<pre><code class="language-C++">#!/usr/bin/python3
"""
这是多行注释,用三个双引号
这是多行注释,用三个双引号
这是多行注释,用三个双引号
"""
print("Hello, World!")
</code></pre>
<h3 id="循环">循环</h3>
<p>循环数组</p>
<pre><code class="language-C++">#第一种,for in的语法
for item in items:
print (item)
#第二种是下标访问
for index in range(len(items)):
print (index,items[index])
#第三种是enumerate,会生成索引和元素
for index,item in enumerate(items):
print (index, item)
</code></pre>
<h2 id="一些框架">一些框架</h2>
<h3 id="django">Django</h3>
<p><a href="https://docs.djangoproject.com/zh-hans/3.1/">官方介绍</a></p>
<p>安装</p>
<pre><code class="language-C++">pip install Django==3.1.5
</code></pre>
<p>安装完成后测试是否成功</p>
<pre><code class="language-Python">>>> import django
>>> print(django.get_version())
3.1
或者
python -m django --version
</code></pre>
<p>创建应用</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python manage.py startapp demo1
</code></pre></div></div>
<p>启动测试服务</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python manage.py runserver
</code></pre></div></div>
<h2 id="第三方库安装">第三方库安装</h2>
<p>国内镜像</p>
<ul>
<li><a href="http://mirrors.aliyun.com/pypi/simple/">阿里云 </a></li>
<li><a href="https://pypi.mirrors.ustc.edu.cn/simple/">中国科技大学</a></li>
<li><a href="http://pypi.douban.com/simple/">豆瓣(douban)</a></li>
<li><a href="https://pypi.tuna.tsinghua.edu.cn/simple/">清华大学</a></li>
<li><a href="http://pypi.mirrors.ustc.edu.cn/simple/">中国科学技术大学</a></li>
</ul>
<h3 id="requests">requests</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install requests
</code></pre></div></div>
<p>正常情况下回安装成功,但是也有意外,比如我在windows10系统上第一次安装时超时</p>
<p>安装失败</p>
<p><img src="/res/img/blog/Python/requests_install_error.png" alt="requests_install" /></p>
<p>说明你采用了默认的pypi源(国外的pypi源),这个很容易出现这种连接超时的问题,所以应当采用国内的镜像源。</p>
<p>在你需要安装的xx后面添加-i + pypi源:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install requests -i http://pypi.douban.com/simple
</code></pre></div></div>
<p>如果还出现下面的情况</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pypi.douban.com is not a trusted or secure host and is being ignored...
</code></pre></div></div>
<p>那么命令就变成这样</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install requests -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
</code></pre></div></div>
<p>惊喜时刻,安装成功了,如下图所示
<img src="/res/img/blog/Python/requests_install_ok.png" alt="requests_install_ok" /></p>
<h3 id="bs4">bs4</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install bs4 -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
</code></pre></div></div>
<h3 id="lxml">lxml</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install lxml -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
</code></pre></div></div>
<h3 id="pandas">pandas</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install --upgrade pandas -i https://pypi.douban.com/simple
</code></pre></div></div>
<h3 id="orm框架">ORM框架</h3>
<h4 id="sqlobject">SQLObject</h4>
<h4 id="storm">Storm</h4>
<h4 id="peewee">peewee</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install peewee
如果安装失败,则可以添加源
pip install peewee -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
</code></pre></div></div>
<p><img src="/res/img/blog/Python/peewee_install_ok.png" alt="peewee_install_ok" /></p>
<h4 id="sqlalchemy">SQLAlchemy</h4>
<h3 id="mkdocs">mkdocs</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install mkdocs
pip install mkdocs -i https://pypi.mirrors.ustc.edu.cn/simple/
</code></pre></div></div>
<p>确认是否安装正确</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdocs --version
</code></pre></div></div>
<p>创建wiki</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdocs new my-wiki
cd my-wiki
</code></pre></div></div>
<p>启动服务</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdocs serve
</code></pre></div></div>
<p>打开浏览器输入127.0.0.1:8000访问wiki</p>
<p>安装主题</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install mkdocs-material
pip install mkdocs-material -i https://pypi.mirrors.ustc.edu.cn/simple/
pip install mkdocs-rtd-dropdown -i https://pypi.mirrors.ustc.edu.cn/simple/
</code></pre></div></div>
<p>在配置文件中进行配置</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>theme:
name: 'material'
</code></pre></div></div>
<p>修改端口号
<a href="https://squidfunk.github.io/mkdocs-material/getting-started/">参考文档</a></p>
<h3 id="scrapy">scrapy</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install scrapy -i https://pypi.mirrors.ustc.edu.cn/simple/
</code></pre></div></div>
<h2 id="常见错误">常见错误</h2>
<h3 id="更新-pip">更新 pip</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>you should consider upgrading via the python -m pip install --upgrade pip command.....
</code></pre></div></div>
<p>解决方案
核心命令</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python -m pip install -U pip
</code></pre></div></div>
<p>上述命令可能会失败,因为超时失败,可以指定国内某个镜像源</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python -m pip install -i http://pypi.douban.com/simple --trusted-host pypi.douban.com -U pip
</code></pre></div></div>
<p><img src="/res/img/blog/Python/pip_install_error.png" alt="pip_install_error" /></p>
<p>进入到提示报错的目录中,删除掉 c:\users\devstone\appdata\local\programs\python\python38\lib\site-packages (20.1)
删除 pip-20.1.dist-info 文件夹,然后重新执行更新命令即可</p>
<p><img src="/res/img/blog/Python/pip_install_ok.png" alt="pip_install_ok" /></p>
<h2 id="参考文章">参考文章</h2>
<ul>
<li><a href="https://www.python.org/">Python 官方</a></li>
</ul>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2021年3月20
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>
<h2 id="捐赠">捐赠</h2>
<center>
<img src="/res/img/myCode.png" width="50%" height="50%" />
</center>
<p>如果觉得分享的内容不错,可以请作者喝杯咖啡.</p>kevin li简介C++(Qt) 和 Word、Excel、PDF 交互总结2022-03-14T00:00:00+00:002022-03-14T00:00:00+00:00http://kevinlq.com/2022/03/14/QtC++%20Word%20Excel%20PDF<blockquote>
<p>阅读本文大概需要 6 分钟</p>
</blockquote>
<p>日常开发软件可能会遇到这类小众需求,导出数据到 <code class="language-plaintext highlighter-rouge">Word</code>、<code class="language-plaintext highlighter-rouge">Excel</code> 以及 <code class="language-plaintext highlighter-rouge">PDF</code>文件,如果你使用 <code class="language-plaintext highlighter-rouge">C++</code> 编程语言,那么可以选择的方案不是很多,恰好最近刚好有这部分需求,整理下这段时间踩过的坑,方便后人</p>
<h2 id="读写-word">读写 Word</h2>
<p>日常开发的软件使用最多的应该是导出数据到 <code class="language-plaintext highlighter-rouge">Word</code> 文档中,目前可以用的方案有这几种</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203131456920.png" alt="" /></p>
<p>没有十全十美的方案,任何方案都存在优点和缺点,下面来详细看下这几种方案的优缺点以及适用场景</p>
<h3 id="xml-模板替换">XML 模板替换</h3>
<blockquote>
<p>原理:事先编辑好一份 <code class="language-plaintext highlighter-rouge">Word</code> 模板,需要替换内容的
地方预留好位置,然后使用特殊字段进行标记,后面使用代码进行全量替换即可完成</p>
</blockquote>
<h4 id="优点">优点</h4>
<ul>
<li>代码量相对较少、导出速度快</li>
<li>跨平台,支持多个系统,系统不安装 office 也能导出;</li>
<li>支持图片以及固定格式导出;</li>
</ul>
<h4 id="缺点">缺点</h4>
<ul>
<li>导出格式固定,可扩展性不强,如果需求变化导出格式变了,那么模板也要跟着改变;</li>
<li>一种格式对应一份模板,如果导出格式较多,需要准备的模板文件较多,这样比较繁琐;</li>
<li>需要 <code class="language-plaintext highlighter-rouge">Word</code> 2003 以上版本;</li>
</ul>
<h4 id="举个栗子">举个栗子</h4>
<p>我们先编辑一份 <code class="language-plaintext highlighter-rouge">Word</code> 模板文档,内容大概如下所示:</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203131659396.png" alt="" /></p>
<ul>
<li>将该文档另存为 <code class="language-plaintext highlighter-rouge">Word XML</code> 文档 <code class="language-plaintext highlighter-rouge">XML-Template.xml</code></li>
<li>读取文档内容进行变量替换</li>
</ul>
<pre><code class="language-C++"> QFile file("XML-Template.xml");
if (!file.open(QIODevice::ReadOnly))
{
qDebug() << "open xxml file fail. " << file.errorString();
return 0;
}
QByteArray baContent = file.readAll();
file.close();
QString strAllContent = QString::fromLocal8Bit(baContent);
strAllContent.replace("$VALUE0", "1");
strAllContent.replace("$VALUE1", QString::fromLocal8Bit("法外狂徒张三"));
strAllContent.replace("$VALUE2", QString::fromLocal8Bit("考试不合格"));
strAllContent.replace("$VALUE3", "2");
strAllContent.replace("$VALUE4", QString::fromLocal8Bit("李四"));
strAllContent.replace("$VALUE5", QString::fromLocal8Bit("合格"));
QFile newFile("export.doc");
if (!newFile.open(QIODevice::WriteOnly))
{
qDebug() << "file open fail." << newFile.errorString();;
return 0;
}
newFile.write(strAllContent.toLocal8Bit());
newFile.close();
</code></pre>
<ul>
<li>保存替换后的内容,写入文件</li>
</ul>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203131827960.png" alt="" /></p>
<p>可以看出来这种方式比较繁琐,重点是编辑固定的模板格式,而且编辑好后保存成<code class="language-plaintext highlighter-rouge">XML</code>格式后还需要继续调整,这种 <code class="language-plaintext highlighter-rouge">XML</code> 格式标签很多,不小心就修改错了,导致导出的文档打不开</p>
<p>这种方式适合模板内容不太复杂,内容较少的情况下使用</p>
<h3 id="com-组件方式">COM 组件方式</h3>
<blockquote>
<p>原理:采用 <code class="language-plaintext highlighter-rouge">Micro Soft</code>公开的接口进行通讯,进行读写时会打开一个 `Word进程来交互</p>
</blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/windows/win32/com/com-technical-overview">COM 技术概述</a></p>
<p><code class="language-plaintext highlighter-rouge">Qt</code> 为我们提供了专门进行交互的类和接口,使用 <code class="language-plaintext highlighter-rouge">Qt ActiveX</code>框架就可以很好的完成交互工作</p>
<h4 id="优点-1">优点</h4>
<ul>
<li>实现简单,快速上手;</li>
</ul>
<h4 id="缺点-1">缺点</h4>
<ul>
<li>导出写入速度慢,因为相当于打开 word 文档操作;</li>
<li>仅 <code class="language-plaintext highlighter-rouge">Windows</code>平台可用,其它平台失效;</li>
<li>需要程序运行的电脑安装 office word,否则调用失败</li>
</ul>
<h4 id="举个栗子-1">举个栗子</h4>
<p>使用时需要引入对应的模块,在 <code class="language-plaintext highlighter-rouge">pro</code> 文件引入模块</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QT *= axcontainer
</code></pre></div></div>
<p>打开文档写入内容</p>
<pre><code class="language-C++">QAxObject *pWordWidget = new(std::nothrow) QAxObject;
bool bResult = pWordWidget->setControl("word.Application");
if (!bResult)
{
return false;
}
// 设置是否显示
pWordWidget->setProperty("Visible", false);
QAxObject *pAllDocuments = pWordWidget->querySubObject("Documents");
if(nullptr == pAllDocuments)
{
return false;
}
// 新建一个空白文档
pAllDocuments->dynamicCall("Add (void)");
// 获取激活的文档并使用
QAxObject *pActiveDocument = pAllDocuments->querySubObject("ActiveDocument");
if(nullptr == pActiveDocument)
{
return false;
}
// 插入字符串
QAxObject *pSelectObj = pWordWidget->querySubObject("Selection");
if (nullptr != pSelectObj)
{
pSelectObj->dynamicCall("TypeText(const QString&)", "公众号:devstone");
}
……
</code></pre>
<p>可以看出来使用起来不难,对于新手友好一点,很多写入操作方法比较繁琐,需要自己重新封装一套接口</p>
<ul>
<li>
<p>这种方案比较适合那些排版比较复杂,图片、文字、表格混排的场景下,而且内容都是动态变化的,可以很好的实现定制化</p>
</li>
<li>当然了它的缺点也不少,也有一些坑,有时候莫名其妙会失败,还有就是比如你电脑安装的 <code class="language-plaintext highlighter-rouge">Word</code> 没有激活,那么每次启动会弹激活窗口</li>
<li>还有就是这种方式要求所有的路径必须是本地化的,比如 <code class="language-plaintext highlighter-rouge">D:\\Soft\test.png</code></li>
<li>使用前最好读取注册表判断当前电脑是否安装了 <code class="language-plaintext highlighter-rouge">Office Word</code>,如果没有安装,直接读取操作肯定会崩溃</li>
</ul>
<p>这种方式同样适用于写入 <code class="language-plaintext highlighter-rouge">Excel</code> 文件,后面再说</p>
<h3 id="html-方式">HTML 方式</h3>
<blockquote>
<p>原理:这种方式得益于 <code class="language-plaintext highlighter-rouge">Word</code>支持 HTML格式导出渲染显示,那么反向也可以支持,需要我们拼接 <code class="language-plaintext highlighter-rouge">HTML</code>格式内容,然后写入文件保存成 <code class="language-plaintext highlighter-rouge">.doc</code>格式</p>
</blockquote>
<h4 id="优点-2">优点</h4>
<ul>
<li>跨平台,不仅限于 <code class="language-plaintext highlighter-rouge">Windows</code>平台,代码可扩展性比较好</li>
<li>导出速度快、代码可扩展;</li>
</ul>
<h4 id="缺点-2">缺点</h4>
<ul>
<li>字符串拼接 <code class="language-plaintext highlighter-rouge">HTML</code> 容易出错,缺失标签导出后无法显示;</li>
<li>插入的图片是本地图片文件的链接,导出的 word文档拷贝到其它电脑图片无法显示</li>
</ul>
<h4 id="举个栗子-2">举个栗子</h4>
<pre><code class="language-C++">QString HTML2Word::getHtmlContent()
{
QString strHtml = "";
strHtml += "<html>";
strHtml += "<head>";
strHtml += "<title>测试生成word文档</title>";
strHtml += "<head>";
strHtml += "<body style=\"bgcolor:yellow\">";
strHtml += "<h1 style=\"background-color:red\">测试qt实现生成word文档</h1>";
strHtml += "<hr>";
strHtml += "<p>这里是插入图片<img src=\"D:\\title.jpg" alt=\"picture\" width=\"100\" height=\"100\"></p>";
strHtml += "</hr>";
strHtml += "</body>";
strHtml += "</html>";
return strHtml;
}
// 保存写入文件
QFile file("D:/htmp2Word.doc");
if (!file.open(QIODevice::WriteOnly))
{
return false;
}
QTextStream out(&file);
out << getHtmlContent();
file.close();
</code></pre>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203131924019.png" alt="" /></p>
<p>这种方式难点在于 <code class="language-plaintext highlighter-rouge">HTML</code>格式拼接,任何缺失字段都会导致导出失败,适合小众需求下导出</p>
<p>图片问题其实可以手动进行转化,文档导出成功后手动拷贝内容到新的文档,这样图片就真正插入到文档中,文档发送给别人也不会丢失图片了</p>
<p>还有一个坑就是:如果你使用 <code class="language-plaintext highlighter-rouge">WPS</code> 打开导出的文档,默认显示的是 <code class="language-plaintext highlighter-rouge">web</code>视图,需要手动进行调整</p>
<p>某些电脑分辨率变化也会导致生成的文档中字体等产生变化</p>
<h3 id="第三方开源库">第三方开源库</h3>
<p>可以使用的第三方库几乎没有,网络上找到的有这么几个</p>
<ul>
<li>OpenOffice: 兼容性差,集成调用难度大</li>
<li>LibOffice: 太庞大,不容易集成</li>
<li>DuckX: 太小众,只能简单的使用</li>
<li>docx:小众库</li>
</ul>
<p><a href="https://github.com/amiremohamadi/DuckX">DuckX库</a>
<a href="https://github.com/lpxxn/docx">docx库</a></p>
<p>在读写 <code class="language-plaintext highlighter-rouge">Word</code>这部分,<code class="language-plaintext highlighter-rouge">C++</code> 基本没有可以使用的第三方库,不像其他语言<code class="language-plaintext highlighter-rouge">Java</code>、<code class="language-plaintext highlighter-rouge">C#</code>、<code class="language-plaintext highlighter-rouge">Python</code>有很多可以选择,这个痛苦也只有 <code class="language-plaintext highlighter-rouge">C++</code> 程序员能够理解了吧</p>
<p>所以怎么选择还是看自己项目需求吧,没有十全十美的方案</p>
<hr />
<p>上面说了这么多,都是导出生成 <code class="language-plaintext highlighter-rouge">Wrod</code>,那么下面来看看有那些方式可以读取显示 <code class="language-plaintext highlighter-rouge">Word</code>内容</p>
<p>这种需求应该不会很多,而且显示难度更大一些</p>
<p>使用 <code class="language-plaintext highlighter-rouge">COM</code>组件方式,即采用 <code class="language-plaintext highlighter-rouge">QAxWidget</code>框架显示 `
office<code class="language-plaintext highlighter-rouge"> 文档内容,本质上就是在我们编写的 </code>Qt<code class="language-plaintext highlighter-rouge"> 界面上嵌入 </code>office<code class="language-plaintext highlighter-rouge"> 的软件,这种方式其实和直接打开 </code>Word`查看没有啥区别,效果、性能上不如直接打开更好一些</p>
<p>目前一般都会采用折中方案,把 <code class="language-plaintext highlighter-rouge">Word</code> 转为 <code class="language-plaintext highlighter-rouge">PDF</code> 进行预览加载显示,我们知道 <code class="language-plaintext highlighter-rouge">PDF</code> 渲染库比较多,生态相对来说要好一些,在选择上就更广泛些,如何使用后面部分有专门介绍 <code class="language-plaintext highlighter-rouge">PDF</code>章节</p>
<h2 id="读写-excel">读写 Excel</h2>
<p>目前有一个支持比较好的第三方库可以使用,整体使用基本可以满足日常使用</p>
<p><a href="https://github.com/QtExcel/QXlsx">QXlsx</a></p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203132115621.png" alt="" /></p>
<p>这款开源库支持跨平台,Linux、Windows、Mac、IOS、Android,使用方式支持动态库调用和源码直接集成,非常方便</p>
<p>编译支持 <code class="language-plaintext highlighter-rouge">qmake</code>和<code class="language-plaintext highlighter-rouge">cmake</code>,可以根据你自己的项目直接集成编译,读写速度非常快</p>
<pre><code class="language-C++">QXlsx::Document xlsx;
// 设置一些样式
QXlsx::Format titleFormat;
titleFormat.setBorderStyle(QXlsx::Format::BorderThin); // 边框样式
titleFormat.setRowHeight(1,1,30); // 设置行高
titleFormat.setHorizontalAlignment(QXlsx::Format::AlignHCenter); // 设置对齐方式
// 插入文本
xlsx.write(1,1, "微信公众号:devstone", titleFormat);
// 合并单元格
xlsx.mergeCells(QXlsx::CellRange(2,1,4,4), titleFormat);
// 导出保存
xlsx.saveAs("D:/xlsx_export.xlsx");
// 添加工作表
xlsx.addSheet("devstone");
</code></pre>
<p>可以看到上手非常容易、各个函数命名也贴近 <code class="language-plaintext highlighter-rouge">Qt Api</code>,是一款非常良心的开源软件</p>
<blockquote>
<p>PS:注意该软件使用 <code class="language-plaintext highlighter-rouge">MIT</code> 许可协议,这样对于很多个人或者公司来说非常良心,意味着你可以无偿使用、修改该项目,但是必须在你项目中也添加同样的 <code class="language-plaintext highlighter-rouge">MIP</code>许可</p>
</blockquote>
<p>上面也提到了,还可以使用 <code class="language-plaintext highlighter-rouge">COM</code> 组件的方式读写 <code class="language-plaintext highlighter-rouge">Excel</code>,不过有了这款开源库基本就可以告别 <code class="language-plaintext highlighter-rouge">COM</code>组件方式了</p>
<h2 id="读写-pdf">读写 PDF</h2>
<p><code class="language-plaintext highlighter-rouge">PDF</code>相关开源库挺多的,给了 <code class="language-plaintext highlighter-rouge">C++</code> 程序员莫大的帮助,目前可用的主要有这些</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203132237709.png" alt="" /></p>
<p>其中 <code class="language-plaintext highlighter-rouge">mupdf</code>和 <code class="language-plaintext highlighter-rouge">poppler</code> 属于功能强大但是很难编译的那种,需要有扎实的三方库编译能力,否则面对 <code class="language-plaintext highlighter-rouge">n</code> 个依赖库会无从下手</p>
<p>不过可喜的是 <code class="language-plaintext highlighter-rouge">Github</code> 上有两个开源库可以供选择</p>
<h3 id="qpdf-库">qpdf 库</h3>
<p>这个库其实封装了 <code class="language-plaintext highlighter-rouge">pdf.js</code>库,使用 <code class="language-plaintext highlighter-rouge">WebEngine</code>来执行 <code class="language-plaintext highlighter-rouge">JavaScript</code>进而加载文件</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203132335058.png" alt="" /></p>
<p><a href="https://github.com/Archie3d/qpdf">项目地址</a></p>
<ul>
<li>直接从本地文件加载;</li>
<li>支持从内存数据直接加载渲染 PDF 内容;</li>
</ul>
<p>这种方式对环境有特殊要求了,如果你的项目使用的 <code class="language-plaintext highlighter-rouge">Qt</code> 版本不支持 <code class="language-plaintext highlighter-rouge">WebEngine</code>,那么就无法使用</p>
<h3 id="qtpdf-库">qtpdf 库</h3>
<p>这个库是 <code class="language-plaintext highlighter-rouge">Qt</code> 官方亲自操刀对第三方库进行了封装,暴露的 <code class="language-plaintext highlighter-rouge">API</code> 和 <code class="language-plaintext highlighter-rouge">Qt</code> 类似,使用起来非常舒服</p>
<p><a href="https://www.qt.io/blog/2017/01/30/new-qtpdf-qtlabs-module">Qt 官方</a></p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203132252485.png" alt="" /></p>
<p>代码结构以及使用 Demo
<img src="https://gitee.com/devstone/imageBed/raw/master/images/202203132258364.png" alt="" /></p>
<h4 id="小试牛刀">小试牛刀</h4>
<p>关于如何使用,官方已经给了我们非常详细的步骤了,直接跟着下面几步就 OK 了</p>
<p><a href="https://wiki.qt.io/Handling_PDF">官方教程</a></p>
<pre><code class="language-C++">git clone git://code.qt.io/qt-labs/qtpdf
cd qtpdf
git submodule update --init --recursive
qmake
make
cd examples/pdf/pdfviewer
qmake
make
./pdfviewer /path/to/my/file.pdf
</code></pre>
<p>可以看到使用了谷歌开源的 <code class="language-plaintext highlighter-rouge">pdfium</code> 三方库,编译时需要单独更新下载这个库,因为某些原因可能你无法下载,不过好在有人在 <code class="language-plaintext highlighter-rouge">GitHub</code>上同步了这个仓库的镜像,有条件还是建议直接下载最新稳定版的</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202203132302785.png" alt="" /></p>
<p>可正常访问的仓库地址:https://github.com/PDFium/PDFium</p>
<p>相关类可以看这个文档:https://developers.foxit.com/resources/pdf-sdk/c_api_reference_pdfium/modules.html</p>
<blockquote>
<p>最后还要注意项目开源协议:pdfium引擎开始来自于福昕,一个中国本土的软件公司,Google与其合作最终进行了开源,目前采用的是 <code class="language-plaintext highlighter-rouge">BSD 3-Clause</code> 协议,这种协议允许开发者自由使用、修改源代码,也可以修改后重新发布,允许闭源进行商业行为,不过需要你在发布的产品中包含原作者代码中的 <code class="language-plaintext highlighter-rouge">BSD</code> 协议</p>
</blockquote>
<h2 id="总结">总结</h2>
<p>以上就是项目中常用的文档处理方法总结,当然了肯定也还有其它方案可以实现,毕竟条条大路通罗马,如果你还要不错的方案和建议欢迎留言</p>
<p>PS: 以上方案和对应的源码编译、使用例子会统一上传到 <code class="language-plaintext highlighter-rouge">GitHub</code>对应的仓库,方便后人使用</p>
<p>取之互联网、回报互联网</p>
<p>原创不易,如果觉得对你有帮助,欢迎点赞、在看、转发</p>
<p><strong>推荐阅读</strong></p>
<ul>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484658&idx=1&sn=55af25cd6e608fa9cb1452610928e71b&chksm=e85c0ac2df2b83d453d80f66fcdefca31e998a8ac0ba0226edbd80b437f99ed5184478805d8e&scene=21#wechat_redirect">Qt Creator 源码学习笔记01,初识QTC</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484676&idx=1&sn=c1bd8cbd78d8e098c22353f567cc3620&chksm=e85c0b34df2b822288a7baa016f4a64df47e5e119039836ff686cb2ad3a7e28a5808efc0c13a&scene=21#wechat_redirect">Qt Creator 源码学习笔记02,认识框架结构结构</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484701&idx=1&sn=c69eb1edfb551c41690938423ca2ef7c&chksm=e85c0b2ddf2b823b1179f216e57ca91b9ce4068a0469e8ba062ab3596e9dc51ac05a1572da85&scene=21#wechat_redirect">Qt Creator 源码学习笔记03,大型项目如何管理工程</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484767&idx=1&sn=971c14682095a8a84a8161311400100f&chksm=e85c0b6fdf2b82799db871eafb6d0b2465c75d8020b1f87a1f5825f82edcad2c051b7d6e1c2c&scene=21#wechat_redirect">Qt Creator 源码学习笔记04,多插件实现原理分析</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484822&idx=1&sn=eff40e19952a534fc0d97cab2417ce9e&chksm=e85c0ba6df2b82b017eb568adacd5b407f269cc2c2e7d2c36609fdda76b029bdcd70003fadba&token=193645825&lang=zh_CN#rd">Qt Creator 源码学习笔记 05,菜单栏是怎么实现插件化的?</a></li>
</ul>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2022年3月14日22:24:15
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>
<h2 id="捐赠">捐赠</h2>
<center>
<img src="https://gitee.com/devstone/imageBed/raw/master/code/myCode.png" width="50%" height="50%" />
</center>
<p>如果觉得分享的内容不错,可以请作者喝杯咖啡.</p>kevin li阅读本文大概需要 6 分钟Qt Creator 源码学习笔记05,菜单栏是怎么实现插件化的?2022-02-14T00:00:00+00:002022-02-14T00:00:00+00:00http://kevinlq.com/2022/02/14/QTC-learn05_menu-plugin<blockquote>
<p>阅读本文大概需要 6 分钟</p>
</blockquote>
<p>对于一个多插件的 <code class="language-plaintext highlighter-rouge">IDE</code> 软件来说,支持界面扩展是必不可少的,今天我们来看看在 <code class="language-plaintext highlighter-rouge">Qt Creator</code> 当中是如何实现界面扩展的</p>
<h2 id="概述">概述</h2>
<p>界面扩展无非就是在其它插件中访问修改主界面当中的一些菜单、参数,或者添加、删除某些菜单,目前很多大型软件都是支持插件化开发的</p>
<p>前几篇我们一起看了<code class="language-plaintext highlighter-rouge">Qt Creator</code>的主界面其实很简单,主界面包括一个菜单栏,模式工具栏,内容区域以及状态栏,如下图所示:</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202112192158402.png" alt="" /></p>
<p>我们看到的其它丰富功能均是通过插件化实现的,今天我们详细学习下看看 <code class="language-plaintext highlighter-rouge">QTC</code> 当中菜单栏是怎么实现扩展的</p>
<h2 id="实现原理">实现原理</h2>
<blockquote>
<p>在学习代码之前我们可以想一想,如果让我们自己来实现应该如何实现,比如扩展一个<code class="language-plaintext highlighter-rouge">Menu</code>菜单?</p>
</blockquote>
<p>既然其他插件要扩展,那么肯定需要访问核心插件创建的 <code class="language-plaintext highlighter-rouge">menu</code> 对象,那么就必须要有访问权限,那么核心插件定义的 <code class="language-plaintext highlighter-rouge">menu</code> 对象应该有哪些权限呢?</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202202082214583.png" alt="" /></p>
<p>仔细回忆下我们刚开始学习 <code class="language-plaintext highlighter-rouge">C/C++</code> 的时候老师就给我们说过,定义一个变量/对象要注意哪些关键点?</p>
<ul>
<li>变量/对象的名</li>
<li>变量/对象的值</li>
<li>变量/对象的作用域</li>
<li>变量/对象的生命周期</li>
</ul>
<p>所以我们要实现一个菜单也是需要考虑这几个方面,最关键的是这个对象的生命周期,外部要能访问该对象可以有好几种方式:暴露指针给外使用、提供注册接口、定义单例……,其实把 <code class="language-plaintext highlighter-rouge">menu</code>定义成一个单例是最便捷最灵活的一种方式了,类似下面这种</p>
<pre><code class="language-C++">class MenuManager
{
public:
static MenuManager * instance();
......
}
</code></pre>
<p>PS: 定义接口或者暴露指针也可以,只不过每次访问还要先访问核心插件对象,处理起来比较繁琐罢了</p>
<h3 id="源码实现">源码实现</h3>
<p>好了,下面我们看下源码是怎么实现的</p>
<p>菜单管理代码主要在这个位置 : /Src/plugins/.coreplugin/actionmanager</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202202082217903.png" alt="" /></p>
<p>文件虽然看着很多,不用担心,我们主要关心的类有这么几个:</p>
<ul>
<li>ActionContainer</li>
<li>ActionContainerPrivate</li>
<li>MenuActionContainer</li>
<li>MenuBarActionContainer</li>
<li>ActionManager</li>
</ul>
<p>这几个类之间继承关系如下所示:</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202202082220274.png" alt="" /></p>
<p>黄色表示的类对内使用,外部看不到具体的实现,每个菜单都可以是一个 <code class="language-plaintext highlighter-rouge">MenuActionContainer</code> 对象,<code class="language-plaintext highlighter-rouge">MenuBarActionContainer</code>全局只有一份,相当于是一个容器来容纳所有的菜单</p>
<p>那么我们如何创建一个菜单呢?其中有专门管理创建、注册的类来实现,这是一个单例类</p>
<pre><code class="language-C++">class CORE_EXPORT ActionManager : public QObject
{
Q_OBJECT
public:
static ActionManager *instance();
// 注册菜单
static ActionContainer *createMenu(Id id);
// 注册菜单栏
static ActionContainer *createMenuBar(Id id);
// 注册管理某个action
static Command *registerAction(QAction *action, Id id,
const Context &context = Context(Constants::C_GLOBAL),
bool scriptable = false);
static void unregisterAction(QAction *action, Id id);
......
}
</code></pre>
<p>在这个单例类当中,主要有两个重要的数据结构用来存储创建的菜单对象,详细实现都在它的 <code class="language-plaintext highlighter-rouge">D</code>指针里面</p>
<pre><code class="language-C++">class ActionManagerPrivate : public QObject
{
Q_OBJECT
public:
typedef QHash<Id, Action *> IdCmdMap;
typedef QHash<Id, ActionContainerPrivate *> IdContainerMap;
......
IdCmdMap m_idCmdMap;
IdContainerMap m_idContainerMap;
}
</code></pre>
<p>使用哈希Map 来存储每个对象,当创建的菜单对象比较多时查找效率非常高,同时注意键值<code class="language-plaintext highlighter-rouge">key</code> 是一个自定义的字符串<code class="language-plaintext highlighter-rouge">ID</code>,由特殊规则构成的全局唯一的值</p>
<pre><code class="language-C++">// 创建菜单
ActionContainer *ActionManager::createMenu(Id id)
{
// 创建前先进行查找,已经存在了直接返回该对象
const ActionManagerPrivate::IdContainerMap::const_iterator it = d->m_idContainerMap.constFind(id);
if (it != d->m_idContainerMap.constEnd())
return it.value();
MenuActionContainer *mc = new MenuActionContainer(id);
d->m_idContainerMap.insert(id, mc);
// 绑定销毁信号,当菜单对象删除后从当前map中移除
connect(mc, &QObject::destroyed, d, &ActionManagerPrivate::containerDestroyed);
return mc;
}
void ActionManagerPrivate::containerDestroyed()
{
ActionContainerPrivate *container = static_cast<ActionContainerPrivate *>(sender());
m_idContainerMap.remove(m_idContainerMap.key(container));
}
</code></pre>
<p>其中有一个比较重要的数据结构 <code class="language-plaintext highlighter-rouge">Context</code></p>
<pre><code class="language-C++">class CORE_EXPORT Context
{
public:
Context() {}
explicit Context(Id c1) { add(c1); }
Context(Id c1, Id c2) { add(c1); add(c2); }
Context(Id c1, Id c2, Id c3) { add(c1); add(c2); add(c3); }
......
void add(const Context &c) { d += c.d; }
void add(Id c) { d.append(c); }
private:
QList<Id> d;
};
</code></pre>
<p>这个类其实就是一个字符串 <code class="language-plaintext highlighter-rouge">ID</code> 的数组封装,各个菜单的标识、状态控制都用到了它,这个结构贯穿整个 <code class="language-plaintext highlighter-rouge">Qt Creator</code>插件系统,使用起来还是非常方便的</p>
<p>有了上面的结构,那么如何创建菜单以及子菜单呢,下面我们详细看下</p>
<h4 id="创建-menubar">创建 MenuBar</h4>
<pre><code class="language-C++"> ActionContainer *menubar = ActionManager::createMenuBar(Constants::MENU_BAR);
// System menu bar on Mac
if (!HostOsInfo::isMacHost())
{
setMenuBar(menubar->menuBar());
}
</code></pre>
<p>这里没啥好说的,和我们平时在<code class="language-plaintext highlighter-rouge">QMainWindow</code>当中创建方法一样,只不过这里创建细节统一封装管理起来了</p>
<h4 id="创建菜单">创建菜单</h4>
<p>下面我们以「文件」菜单为例看下创建过程</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202202082238269.png" alt="" /></p>
<pre><code class="language-C++"> // File Menu
ActionContainer *filemenu = ActionManager::createMenu(Constants::M_FILE);
menubar->addMenu(filemenu, Constants::G_FILE);
filemenu->menu()->setTitle(tr("&File"));
</code></pre>
<p>这两行代码就完成了「文件」菜单的创建,代码很简洁也非常容易理解,这里我们需要注意下几个常量定义技巧</p>
<pre><code class="language-C++">const char M_FILE[] = "QtCreator.Menu.File";
// Main menu bar groups
const char G_FILE[] = "QtCreator.Group.File";
</code></pre>
<p>所有的菜单都是通过字符串常量来区分的,这个常量相当于现实世界中我们每个人的身份证都是唯一的,而且都是有规律的</p>
<p>PS:看到这里再问大家一个问题,定义常量时,宏定义写法和上面的写法哪个好?为什么?欢迎讨论</p>
<pre><code class="language-C++">#define G_FILE "QtCreator.Group.File"
const char G_FILE[] = "QtCreator.Group.File";
</code></pre>
<p>到了这里,仅仅是创建了菜单,点击菜单后内容还是空的,我们接着继续看</p>
<pre><code class="language-C++">
void MainWindow::registerDefaultActions()
{
// 从单例类中获取上一步创建的菜单容器类
ActionContainer *mfile = ActionManager::actionContainer(Constants::M_FILE);
// 添加分隔符
mfile->addSeparator(Constants::G_FILE_SAVE);
mfile->addSeparator(Constants::G_FILE_PRINT);
mfile->addSeparator(Constants::G_FILE_CLOSE);
mfile->addSeparator(Constants::G_FILE_OTHER);
// 创建每个action
QIcon icon = QIcon::fromTheme(QLatin1String("document-new"), Utils::Icons::NEWFILE.icon());
m_newAction = new QAction(icon, tr("&New File or Project..."), this);
cmd = ActionManager::registerAction(m_newAction, Constants::NEW);
cmd->setDefaultKeySequence(QKeySequence::New);
mfile->addAction(cmd, Constants::G_FILE_NEW);
......
}
</code></pre>
<p>每个<code class="language-plaintext highlighter-rouge">action</code>创建后通过 <code class="language-plaintext highlighter-rouge">addAction</code> 添加到对应的菜单上即可,如果某个 <code class="language-plaintext highlighter-rouge">action</code> 还有子菜单,那么就需要先创建一个菜单,然后直接添加菜单即可,比如「最近访问的文件」</p>
<p><img src="https://gitee.com/devstone/imageBed/raw/master/images/202202082311314.png" alt="" /></p>
<pre><code class="language-C++"> ActionContainer *ac = ActionManager::createMenu(Constants::M_FILE_RECENTFILES);
mfile->addMenu(ac, Constants::G_FILE_OPEN);
ac->menu()->setTitle(tr("Recent &Files"));
ac->setOnAllDisabledBehavior(ActionContainer::Show);
</code></pre>
<p>任意一个<code class="language-plaintext highlighter-rouge">action</code>可以拥有多个子菜单,只需要在创建的时候根据递归关系选择创建<code class="language-plaintext highlighter-rouge">action</code>还是<code class="language-plaintext highlighter-rouge">ActionContainer</code></p>
<h2 id="测试">测试</h2>
<p>为了验证上述流程分析是否正确,我们可以编译一个测试插件,然后在该插件里面新创建一个菜单,分为下面几个流程:</p>
<ul>
<li>创建测试插件<code class="language-plaintext highlighter-rouge">PluginDemo</code>子工程;</li>
<li>在插件初始化函数当中创建菜单;</li>
<li>编译该插件,然后把该插件(动态库)拷贝到 <code class="language-plaintext highlighter-rouge">QTC</code> 对应插件目录下</li>
<li>运行软件</li>
</ul>
<p>创建插件编译后生成的目录结构如下所示:
<img src="https://gitee.com/devstone/imageBed/raw/master/images/202202132133546.png" alt="" /></p>
<p>可以看到我们测试插件路径和程序 <code class="language-plaintext highlighter-rouge">exe</code>是独立的</p>
<p>运行软件显示效果如下所示
<img src="https://gitee.com/devstone/imageBed/raw/master/images/202202102221250.png" alt="" /></p>
<p>可以看到整个代码不超过 10行就把创建的菜单添加到了主界面当中,使用起来目前看来还是很方便的,而且方便扩展,由于使用插件化和其它模块进行了解耦</p>
<p>相信大家也都看到了,<code class="language-plaintext highlighter-rouge">QTC</code> 插件系统当中比较重要的<code class="language-plaintext highlighter-rouge">ID</code>编号问题,这些编号都有固定的格式,而且每个<code class="language-plaintext highlighter-rouge">ID</code>无论从命名还是具体内容表达的意思都是显而易见的</p>
<pre><code class="language-C++">const char M_FILE[] = "QtCreator.Menu.File";
const char M_EDIT[] = "QtCreator.Menu.Edit";
const char M_EDIT_ADVANCED[] = "QtCreator.Menu.Edit.Advanced";
const char M_TOOLS[] = "QtCreator.Menu.Tools";
const char G_FILE_NEW[] = "QtCreator.Group.File.New";
const char G_FILE_OPEN[] = "QtCreator.Group.File.Open";
const char G_FILE_PROJECT[] = "QtCreator.Group.File.Project";
const char G_FILE_SAVE[] = "QtCreator.Group.File.Save";
</code></pre>
<ul>
<li><code class="language-plaintext highlighter-rouge">M</code>开头表示菜单名字,比如文件、编辑、视图、构建……</li>
<li><code class="language-plaintext highlighter-rouge">G</code>开头表示分组信息,比如文件菜单当中包含了:新建文件、打开文件、打开工程、保存文件……</li>
</ul>
<h2 id="总结">总结</h2>
<p><code class="language-plaintext highlighter-rouge">Qt Creator</code>界面插件化内容还很多,本次只是简简单单地学习了菜单管理逻辑以及如何使用,如果想了解更多细节阅读对应源码即可</p>
<p>一款优秀的开源软件有很多内容值得我们反复去学习、理解、使用的,未来很长,我们继续……</p>
<hr />
<p>PS:文中涉及到相关流程图以及对应源码,如果感兴趣可以后台私信发给你</p>
<p>如果觉得对你有帮助,欢迎留言互相交流学习</p>
<h2 id="相关阅读">相关阅读</h2>
<ul>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484658&idx=1&sn=55af25cd6e608fa9cb1452610928e71b&chksm=e85c0ac2df2b83d453d80f66fcdefca31e998a8ac0ba0226edbd80b437f99ed5184478805d8e&scene=21#wechat_redirect">Qt Creator 源码学习笔记01,初识QTC</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484676&idx=1&sn=c1bd8cbd78d8e098c22353f567cc3620&chksm=e85c0b34df2b822288a7baa016f4a64df47e5e119039836ff686cb2ad3a7e28a5808efc0c13a&scene=21#wechat_redirect">Qt Creator 源码学习笔记02,认识框架结构结构</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484701&idx=1&sn=c69eb1edfb551c41690938423ca2ef7c&chksm=e85c0b2ddf2b823b1179f216e57ca91b9ce4068a0469e8ba062ab3596e9dc51ac05a1572da85&scene=21#wechat_redirect">Qt Creator 源码学习笔记03,大型项目如何管理工程</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484767&idx=1&sn=971c14682095a8a84a8161311400100f&chksm=e85c0b6fdf2b82799db871eafb6d0b2465c75d8020b1f87a1f5825f82edcad2c051b7d6e1c2c&scene=21#wechat_redirect">Qt Creator 源码学习笔记04,多插件实现原理分析</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzY5NTQ1NQ==&mid=2247484822&idx=1&sn=eff40e19952a534fc0d97cab2417ce9e&chksm=e85c0ba6df2b82b017eb568adacd5b407f269cc2c2e7d2c36609fdda76b029bdcd70003fadba&token=193645825&lang=zh_CN#rd">Qt Creator 源码学习笔记 05,菜单栏是怎么实现插件化的?</a></li>
</ul>
<hr />
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>作者:鹅卵石
时间: 2022年2月14日21:43:20
版本:V 0.0.1
邮箱:kevinlq@163.com
版权:本博客若无特别声明,均属于作者原创文章,欢迎大家转载分享。但是,
希望您注明来源,并留下原文地址,这是对作者最大的尊重,也是对知识的尊重。
</code></pre></div></div>
<!-- more -->
<hr />
<p><strong>如果您对本文有任何问题,可以在下方留言,或者Email我.</strong></p>
<h2 id="捐赠">捐赠</h2>
<center>
<img src="https://gitee.com/devstone/imageBed/raw/master/code/myCode.png" width="50%" height="50%" />
</center>
<p>如果觉得分享的内容不错,可以请作者喝杯咖啡.</p>kevin li阅读本文大概需要 6 分钟