昨天凌晨完成了毕业设计的客户端,其实就是一个小工具,配合服务端使用,这里总结一下遇到的问题和解决方法。
既然已经发布了 2.0-stable
版本,那么我就使用他来写这篇文章了。
打包
首先就是打包的过程,之前我没有打包过完整的安装包,只是用官方工具发布 Release 版本。我这次将 innoSetup
将 Release 发布的程序打包成了安装包。
innoSetup
Inno Setup 是一款免费的 Windows 安装程序制作工具,由 Jordan Russell 开发。它是创建 Windows 安装包最流行的工具之一,以其小巧、高效和功能强大而著称。
特点:
- 免费开源:遵循 MIT 许可证,完全免费使用
- 轻量高效:生成的安装程序体积小,运行速度快
- 功能全面:
- 支持创建快捷方式
- 支持注册表操作
- 支持多语言安装界面
- 支持静默安装
- 支持自定义安装页面
- 脚本驱动:使用类似 Pascal 的脚本语言定义安装过程
- 支持现代 Windows:兼容 Windows 11/10/8/7 等版本
打包方法
我问了一下 Deepseek,但是他给了我一坨教程,看着晕,我这里就放上文配图最简单的办法。
然后下面看哪里需要修改,修改好后下一步直到这里。这里要注意,打包的时候如果 exe 在 release 文件夹里也会被作为文件压缩,可以把他拖出来然后再添加文件夹。
一路下一步到这里,过程中看哪里需要修改就修改,其他保持默认就好。第一个是输出文件夹,第二个是输出的名字,第三个是图标。
然后一路下一步就可以了,这样就打包完成了。
关于HTTP
HTTP 协议有很多请求方式,我这里主要用到的是 POST 请求。我这里是使用了单例模式,写了个 HTTP 管理类,因为 QNetworkAccessManager
本身支持异步请求,所以就没有进行二次封装。下面是一个 POST接口的调用代码。
void TCHttpService::apiLogin()
{
QByteArray pwdMd5 = QCryptographicHash::hash(m_firstPwd.toUtf8(), QCryptographicHash::Md5);
QString md5Hex = pwdMd5.toHex();
QString urlStr;
if (m_enableSsl)
urlStr = "https://" + m_domain + "/api/login";
else
urlStr = "http://" + m_domain + "/api/login";
QUrl url(urlStr);
QJsonObject jsonObj;
jsonObj["user"]=m_userName;
jsonObj["pwd"]=md5Hex;
QJsonDocument jsonDoc(jsonObj);
QByteArray jsonData = jsonDoc.toJson();
QMap<QString, QString> headers;
headers.insert("Content-Type", "application/json");
QNetworkReply* reply = nullptr;
QNetworkRequest request(url);
for (auto ite = headers.constBegin(); ite != headers.constEnd(); ite++){
request.setRawHeader(ite.key().toUtf8(), ite.value().toUtf8());
}
reply = m_manager.post(request, jsonData);
QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
if (jsonDoc.isEmpty())
return;
QJsonObject jsonObj = jsonDoc.object();
int code = jsonObj["code"].toInt();
if (code == 0) {
this->m_token = jsonObj["token"].toString();
qDebug() << this->m_token;
m_isOnline = true;
ImageManager::instance()->addDomainUser(m_domain, m_userName);
QString stdPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
ImageManager::instance()->initialize(stdPath + "/picpanel.db");
emit signal_loginSuc();
}
});
}
上传数据是使用另外一种数据包组织形式,可以获取上传进度。
void TCHttpService::apiUpload(const QString &filePath)
{
QString urlStr;
if (m_enableSsl)
urlStr = "https://" + m_domain + "/api/upload";
else
urlStr = "http://" + m_domain + "/api/upload";
QFile *file = new QFile(filePath);
if (!file->open(QIODevice::ReadOnly)) {
qDebug() << "无法打开文件:" << filePath;
delete file;
return;
}
// 计算文件 MD5
QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(file);
QString md5 = hash.result().toHex();
// 重置文件指针
file->seek(0);
// 获取文件大小
qint64 fileSize = file->size();
// 创建 multipart 请求
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
#else
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormData);
#endif
// 添加文件部分
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"file\"; filename=\"" + QFileInfo(filePath).fileName() + "\""));
filePart.setBodyDevice(file);
multiPart->append(filePart);
// 添加用户信息部分
QHttpPart userPart;
userPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"user\""));
userPart.setBody(m_userName.toUtf8());
multiPart->append(userPart);
// 添加 MD5 部分
QHttpPart md5Part;
md5Part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"md5\""));
md5Part.setBody(md5.toUtf8());
multiPart->append(md5Part);
// 添加文件大小部分
QHttpPart sizePart;
sizePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"size\""));
sizePart.setBody(QString::number(fileSize).toUtf8());
multiPart->append(sizePart);
// 创建网络请求
QNetworkRequest request(urlStr); // 替换为实际的服务器地址
request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=" + multiPart->boundary());
// 发送请求
// QNetworkAccessManager *manager = new QNetworkAccessManager(this);
QNetworkReply *reply = m_manager.post(request, multiPart);
// 连接信号槽
connect(reply, &QNetworkReply::finished, this, [=]() {
if (reply->error() == QNetworkReply::NoError) {
qDebug() << "上传成功:";
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
QJsonObject jsonObj = jsonDoc.object();
QString fileUrl = jsonObj["url"].toString();
ImageManager::instance()->addImageUrl(m_domain, m_userName, fileUrl);
emit signal_uploadFileSec(fileUrl);
} else {
qDebug() << "上传失败:" << reply->errorString();
}
reply->deleteLater();
multiPart->deleteLater();
});
connect(reply, &QNetworkReply::uploadProgress, this, [this](qint64 bytesSent, qint64 bytesTotal) {
if (bytesTotal > 0) {
int progress = (bytesSent * 100) / bytesTotal;
emit signal_progressUpdate(progress);
qDebug() << "上传进度:" << progress << "%";
}
});
}
关于 QSS
这次在开发过程中除了常规的 QSS 意外,用了一些其他的,比如 复选框的下拉框
和 checkbox
。
QCheckBox {
spacing: 0px;
padding: 0px;
margin: 0px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
}
QCheckBox::indicator:unchecked {
image: url(:/qrc/image/checkbox_unchecked.png)
}
QCheckBox::indicator:checked {
image: url(:/qrc/image/checkbox_checked.png)
}
QCheckBox::indicator:indeterminate {
image: url(:/qrc/image/checkbox_parcial.png)
}
QComboBox QAbstractItemView {
border-style: none;
border-radius: 10px;
outline: 0;
}
QComboBox QAbstractItemView::item {
padding: 0px;
margin: 0px;
border-style: none;
}
QComboBox QAbstractItemView::item:selected {
padding: 0px;
margin: 0px;
border-style: none;
color: black;
background-color: #F5F7FA;
}
QComboBox QAbstractItemView::item:hover {
padding: 0px;
margin: 0px;
background-color: #F5F7FA;
color: black;
border-style: none;
}
使用资源图片可以达到自定义复选框的效果。
未选中状态:
选中状态:
数据库
我的桌面软件使用 本地优先
原则,本地数据存储在 SQLite 数据库里。使用轻量级数据库比单纯的文件存储:如 JSON,TXT 等在大量文件存储场景下效果高且方便管理。
#include "urldatabase.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
ImageManager* ImageManager::m_instance = nullptr;
QMutex ImageManager::m_mutex;
ImageManager::ImageManager(QObject *parent) : QObject(parent)
{
}
ImageManager::~ImageManager()
{
if (m_db.isOpen()) {
m_db.close();
}
}
ImageManager* ImageManager::instance()
{
QMutexLocker locker(&m_mutex);
if (!m_instance) {
m_instance = new ImageManager();
}
return m_instance;
}
bool ImageManager::initialize(const QString &dbPath)
{
m_db = QSqlDatabase::addDatabase("QSQLITE", "image_manager_connection");
m_db.setDatabaseName(dbPath);
if (!m_db.open()) {
qWarning() << "Failed to open database:" << m_db.lastError().text();
return false;
}
return createTables();
}
bool ImageManager::createTables()
{
QSqlQuery query(m_db);
// 创建域名表
if (!query.exec("CREATE TABLE IF NOT EXISTS domains ("
"domain_id INTEGER PRIMARY KEY AUTOINCREMENT, "
"domain TEXT NOT NULL UNIQUE)")) {
qWarning() << "Failed to create domains table:" << query.lastError().text();
return false;
}
// 创建用户表
if (!query.exec("CREATE TABLE IF NOT EXISTS users ("
"user_id INTEGER PRIMARY KEY AUTOINCREMENT, "
"username TEXT NOT NULL UNIQUE)")) {
qWarning() << "Failed to create users table:" << query.lastError().text();
return false;
}
// 创建域名-用户关联表(确保唯一)
if (!query.exec("CREATE TABLE IF NOT EXISTS domain_users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"domain_id INTEGER NOT NULL, "
"user_id INTEGER NOT NULL, "
"UNIQUE(domain_id, user_id), "
"FOREIGN KEY(domain_id) REFERENCES domains(domain_id), "
"FOREIGN KEY(user_id) REFERENCES users(user_id))")) {
qWarning() << "Failed to create domain_users table:" << query.lastError().text();
return false;
}
// 创建图片URL表
if (!query.exec("CREATE TABLE IF NOT EXISTS image_urls ("
"url_id INTEGER PRIMARY KEY AUTOINCREMENT, "
"domain_user_id INTEGER NOT NULL, "
"image_url TEXT NOT NULL, "
"upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "
"FOREIGN KEY(domain_user_id) REFERENCES domain_users(id), "
"UNIQUE(domain_user_id, image_url))")) {
qWarning() << "Failed to create image_urls table:" << query.lastError().text();
return false;
}
return true;
}
bool ImageManager::addDomain(const QString &domain)
{
QSqlQuery query(m_db);
query.prepare("INSERT OR IGNORE INTO domains (domain) VALUES (?)");
query.addBindValue(domain);
return query.exec();
}
bool ImageManager::addUser(const QString &user)
{
QSqlQuery query(m_db);
query.prepare("INSERT OR IGNORE INTO users (username) VALUES (?)");
query.addBindValue(user);
return query.exec();
}
int ImageManager::getDomainId(const QString &domain)
{
QSqlQuery query(m_db);
query.prepare("SELECT domain_id FROM domains WHERE domain = ?");
query.addBindValue(domain);
if (query.exec() && query.next()) {
return query.value(0).toInt();
}
return -1;
}
int ImageManager::getUserId(const QString &user)
{
QSqlQuery query(m_db);
query.prepare("SELECT user_id FROM users WHERE username = ?");
query.addBindValue(user);
if (query.exec() && query.next()) {
return query.value(0).toInt();
}
return -1;
}
bool ImageManager::addDomainUser(const QString &domain, const QString &user)
{
if (!addDomain(domain) || !addUser(user)) {
return false;
}
int domainId = getDomainId(domain);
int userId = getUserId(user);
if (domainId == -1 || userId == -1) {
return false;
}
QSqlQuery query(m_db);
query.prepare("INSERT OR IGNORE INTO domain_users (domain_id, user_id) VALUES (?, ?)");
query.addBindValue(domainId);
query.addBindValue(userId);
return query.exec();
}
bool ImageManager::addImageUrl(const QString &domain, const QString &user, const QUrl &imageUrl)
{
int domainId = getDomainId(domain);
int userId = getUserId(user);
if (domainId == -1 || userId == -1) {
return false;
}
QSqlQuery query(m_db);
query.prepare("SELECT id FROM domain_users WHERE domain_id = ? AND user_id = ?");
query.addBindValue(domainId);
query.addBindValue(userId);
if (!query.exec() || !query.next()) {
return false;
}
int domainUserId = query.value(0).toInt();
query.prepare("INSERT OR IGNORE INTO image_urls (domain_user_id, image_url) VALUES (?, ?)");
query.addBindValue(domainUserId);
query.addBindValue(imageUrl.toString());
return query.exec();
}
bool ImageManager::removeImageUrl(const QString &domain, const QString &user, const QUrl &imageUrl)
{
int domainId = getDomainId(domain);
int userId = getUserId(user);
if (domainId == -1 || userId == -1) {
return false;
}
QSqlQuery query(m_db);
query.prepare("SELECT id FROM domain_users WHERE domain_id = ? AND user_id = ?");
query.addBindValue(domainId);
query.addBindValue(userId);
if (!query.exec() || !query.next()) {
return false;
}
int domainUserId = query.value(0).toInt();
query.prepare("DELETE FROM image_urls WHERE domain_user_id = ? AND image_url = ?");
query.addBindValue(domainUserId);
query.addBindValue(imageUrl.toString());
return query.exec();
}
QList<QUrl> ImageManager::getImageUrls(const QString &domain, const QString &user)
{
QList<QUrl> urls;
int domainId = getDomainId(domain);
int userId = getUserId(user);
if (domainId == -1 || userId == -1) {
return urls;
}
QSqlQuery query(m_db);
query.prepare("SELECT i.image_url FROM image_urls i "
"JOIN domain_users du ON i.domain_user_id = du.id "
"WHERE du.domain_id = ? AND du.user_id = ? "
"ORDER BY i.upload_time DESC");
query.addBindValue(domainId);
query.addBindValue(userId);
if (query.exec()) {
while (query.next()) {
urls.append(QUrl(query.value(0).toString()));
}
}
return urls;
}
bool ImageManager::containsDomainUser(const QString &domain, const QString &user)
{
int domainId = getDomainId(domain);
int userId = getUserId(user);
if (domainId == -1 || userId == -1) {
return false;
}
QSqlQuery query(m_db);
query.prepare("SELECT 1 FROM domain_users WHERE domain_id = ? AND user_id = ?");
query.addBindValue(domainId);
query.addBindValue(userId);
return query.exec() && query.next();
}
展望
- ☐ 首先完善打磨一下细节,比如跨平台适配,我在 Mac 上测试过,通知有点问题,另外一些 Qt 无法提供的接口需要调用系统 API。
- ☐ 尝试适配一些其他 OSS 存储桶和 S3等接口,这个可以参考 PicGO 项目。站在巨人的肩膀上,出个一个分支也好。
- ☐ 我希望做一个 Obsidian 插件,因为我日常使用 Obsidian。
- ☐ 希望有更多人可以使用我的图床服务,或者参与进来。
评论