为什么写此系列文章?
本人在学校至工作到现在十余年时间,使用.net C# 开发语言,结合在公司实际开发,和市场的需求中,NET.开发的商业企业级系统遇到的缺点有如下:
1.程序首次加载慢,因为虚拟机编译的原因。
2.WINFORM界面开发不够炫丽,精美。
3.WINFORM界面设计人员难找。
4.程序可以被反编译。
5.安装包过大,部署麻烦,framework.
6.跨平台不够好。
结论:
结合近年来前端设计的走向,最终选择了qt+vue+element UI+sqlite(数据库根据需要情况选择)
qt负责接口和硬件处理
sqlite做数据存储
vue+element UI 实现前端。
结果预览
qt 实现MVC Api控制器开发 web api接口-连载【5】-企业级系统开发实战连载系列 -技术栈(vue、element-ui、qt、c++、sqlite)
使用微软mvc 的同学都知道编写一个web api 是非常方便的,本文通过使用qt 实现 Controller 和action 开发web api。
MVC 结构图如下:
下载QtWebApp(本文最后有下载方式),集成到项目中,这个库主要是实现http 协议,和 QT Web Api库支持。
对于单机版本,不需要用户安装iis 或 tomcat,简化了部署的难度。
QtWebApp目录结构如下:
把QtWebApp解压放到BitPos(上一章节创建的项目)目录下。
效果如下:
打开上一章节创建的解决方案:BitPos 。
在解决方案资源管理器,右键》添加》现有项目,如下:
把BitPos.vcxproj 添加现在项目。
如下图:
选择QtWebApp ,右键》属性
在常规选项中修改sdk 版本,和平台工具集,配置类型,输出为静态库,如下:
在qt project settings 选项中,修改 qt installation 为我们上一节配置的qt版本,如下:
编译源码,这里能够一次编译成功,如下图:
点击BitPos项目,右键》添加》引用
为BitPos添加包含目录,因为后面会用QtWebApp的头文件。
选择BitPos 》属性》vc++目录》包含目录》输入QtWebApp 如下图:
修改BitPos运行库为静态链接。
按上面的步骤,修改这2个项目的配置为release 模式,重复一次操作。否则release编译会报错。
接下来演示如何添加一个用户登入时密码验证的接口。
这个接口有2个参数,分别是user_code ,password
返回为json ,返回了用户代码和用户名称,分别是user_code,user_name
请求描述如下图:
接下来进行实操:添加API 控制器和登入接口Login的方法
新增 src 目录,然后在目录下创建 controller
如下图:
选中controller 右键》添加》Add qt class
输入类名:ApiController
如下图:
点add 如下图:
点击next:如下图
把Base class 修改为HttpRequestHandler
最后贴出这2个类的源码分别是:
ApiController.h的代码如下:
1 #pragma once
2 //解决中文乱码问题。
3 #pragma execution_character_set("utf-8")
4 #include <httpserver/httprequesthandler.h>
5
6 using namespace stefanfrings;
7
8 //控制器 http://localhost:5050/vue-element-admin/api
9 class ApiController :
10 public HttpRequestHandler
11 {
12
13 Q_OBJECT
14 public:
15 Q_INVOKABLE ApiController(const ApiController & v)
16 {
17 *this = v;
18 }
19 Q_INVOKABLE ApiController &operator=(const ApiController &v)
20 {
21 return *this;
22 }
23
24 Q_INVOKABLE ApiController()
25 {
26
27 }
28
29 //action 登入接口。http://localhost:5050/vue-element-admin/api/login
30 Q_INVOKABLE void Login(HttpRequest & request, HttpResponse & response);
31
32 Q_INVOKABLE void ApiResult(const QString& msg, int code,HttpRequest & request, HttpResponse & response);
33 /** Generates the response */
34 Q_INVOKABLE void service(HttpRequest& request, HttpResponse& response);
35
36 void Result(QString msg, int code, HttpResponse & response);
37
38 ~ApiController();
39 };
ApiController.cpp的代码如下:
1 #include "ApiController.h"
2 #include <qjsondocument.h>
3 #include <qjsonobject.h>
4 #include <qcryptographichash.h>
5 #include <qlist.h>
6
7
8
9
10
11
12
13 Q_INVOKABLE void ApiController::Login(HttpRequest & request, HttpResponse & response)
14 {
15 //request.
16
17 //获取post请求的表单 。
18 QMultiMap<QByteArray, QByteArray> forms = request.getParameterMap();
19 //获取用户提交表单中的user_code
20 auto usercode = forms.value("user_code").trimmed();
21 if (usercode.isEmpty())
22 {
23 Result("用户代码不能为空!", 1, response);
24 return;
25 }
26 //获取用户提交表单中的password
27 auto password = forms.value("password").trimmed();
28 if (password.isEmpty())
29 {
30 Result("密码不能为空!", 1, response);
31 return;
32 }
33 //二次判断
34 QByteArray hash = QCryptographicHash::hash(password, QCryptographicHash::Algorithm::Sha256).toBase64();
35
36 //验证用户代码和密码。
37
38 QJsonObject object
39 {
40 {"code", 0},
41 {"msg", QJsonValue::Null}
42 };
43 QJsonObject user
44 {
45 {"user_code", "admin"},
46 {"user_name", "admin"}
47 };
48 object.insert("user", user);
49
50 QJsonDocument usermodel(object);
51
52 //返回用户信息。
53 QByteArray body = usermodel.toJson(QJsonDocument::JsonFormat::Indented);
54
55 response.write(body, true);
56 }
57
58
59 Q_INVOKABLE void ApiController::service(HttpRequest & request, HttpResponse & response)
60 {
61
62 }
63
64 void ApiController::Result(QString msg, int code, HttpResponse & response)
65 {
66 QJsonObject object
67 {
68 {"code", code},
69 {"msg", QJsonValue(msg)}
70 };
71
72
73 QJsonDocument usermodel(object);
74
75 QByteArray body = usermodel.toJson(QJsonDocument::JsonFormat::Indented);
76
77 response.write(body, true);
78 }
79
80 Q_INVOKABLE void ApiController::ApiResult(const QString& msg,int code,HttpRequest & request, HttpResponse & response)
81 {
82 Result(msg, code, response);
83 }
84
85
86
87 ApiController::~ApiController()
88 {
89 }
这2个文件的代码属于业务类的代码,都比较简单,实现了用户登入,用户名和密码的验证,上面已经有注释,这里就不进行重点讲解。
添加通用控制器处理类 requestmapper.cpp ,该类与业务无关。
主要实现接收请求,并查找控制器和接口方法,并进行转发请求,文件内容如下:
1 /**
2 @file
3 @author Stefan Frings
4 */
5
6 #include <QCoreApplication>
7 #include "requestmapper.h"
8 #include "controller/ApiController.h"
9 #include <qmetaobject.h>
10
11 //静态变量
12 QMultiMap<QString, QString> RequestMapper :: Area;
13 template <typename T>
14 int RegisterController(const char *typeName,const QString& area)
15 {
16 QByteArray tmp=typeName;
17 tmp = tmp.toLower();
18 auto type= tmp.constData();
19
20 int v=qRegisterMetaType<T>(type);
21 if (!area.isEmpty())
22 {
23 RequestMapper::RegisterArea(area, type);
24 }
25 return v;
26 }
27 template <typename T>
28 int RegisterController(const QString& area)
29 {
30 return RegisterController<T>(T::staticMetaObject.className(), area);
31 }
32 RequestMapper::RequestMapper(QObject* parent)
33 :HttpRequestHandler(parent)
34 {
35 qDebug("RequestMapper: created");
36 //注册Api控制器,域为vue-element-admin。访问格式为:
37 //http://localhost:5050/{域}/{控制器}
38 //例如 http://localhost:5050/vue-element-admin/api
39 RegisterController<ApiController>("apicontroller", "vue-element-admin");
40 }
41
42
43 RequestMapper::~RequestMapper()
44 {
45 qDebug("RequestMapper: deleted");
46 }
47
48 void RequestMapper::RegisterArea(const QString &area, const QString& classname)
49 {
50 Area.insertMulti(area, classname);
51 }
52
53 //查找控制器,并调用接口方法。
54 void RequestMapper::service(HttpRequest& request, HttpResponse& response)
55 {
56 QByteArray path=request.getPath().toLower();
57 qDebug("RequestMapper: path=%s",path.data());
58 fprintf(stderr, "request: %s\n", path.data());
59 //实现跨域访问,js 调用API 提供了支持。
60 response.setHeader("Connection", "keep-alive");
61 auto origin = request.getHeader("Origin");
62 response.setHeader("Access-Control-Allow-Origin", origin);
63 response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
64 response.setHeader("Access-Control-Allow-Headers", "X-PINGOTHER,Content-Type,x-token");
65 response.setHeader("Access-Control-Max-Age", "86400");
66 response.setHeader("Vary", "Accept-Encoding,Origin");
67 response.setHeader("Keep-Alive", "timeout=2,max=99");
68 //set api header
69 response.setHeader("Content-Type", "application/json; charset=utf-8");
70 //response.setHeader("Access-Control-Allow-Origin", "*"); // also important , if not set , the html application wont run.
71 if (request.getMethod() == "OPTIONS")
72 {
73 response.setStatus(200,"OK");
74 qDebug("RequestMapper: finished request");
75 // Clear the log buffer
76
77 return;
78 }
79 else
80 {
81
82 }
83
84 // For the following pathes, each request gets its own new instance of the related controller.
85 QByteArrayList items = path.split('/');
86 QByteArray areaname;
87 QByteArray controlname;
88 QByteArray actionname;
89 QByteArray a, b, c;
90
91 for (int i = 0; i < items.length(); i++)
92 {
93 QByteArray first = items[i];
94 if (first.isEmpty())
95 continue;
96 else
97 {
98 //get control and action of name.
99 a = first;
100 if(i+1<items.length())
101 b = items[i + 1].toLower();
102
103 if (i + 2 < items.length())
104 c = items[i + 2].toLower();
105 break;
106 }
107 }
108 QList<QString> controls;
109 //判断是否是路由。
110 if (Area.contains(a))
111 {
112 areaname = a;
113 controlname = b;
114 actionname = c;
115 controls=Area.values(a);
116 }
117 else
118 {
119 controlname = a;
120 actionname = b;
121 }
122
123 QString className = (controlname + "Controller").toLower();
124
125 int id = QMetaType::type(className.toLatin1());
126 HttpRequestHandler* result = NULL;
127 //判断area
128 if (id != QMetaType::UnknownType)
129 {
130 if (controls.count() > 0 && !controls.contains(className))
131 {
132
133 qDebug("RequestMapper: finished request");
134
135 return;
136 }
137 }
138
139 if (id != QMetaType::UnknownType)
140 {
141 result = static_cast<HttpRequestHandler*>(QMetaType::create(id));
142 const QMetaObject * theMetaObject = result->metaObject();
143 int nMetathodCount = theMetaObject->methodCount();
144 QByteArray method;
145 //查找方法
146 for (int nMetathodIndex = 0; nMetathodIndex < nMetathodCount; nMetathodIndex++)
147 {
148 QByteArray oneMethod = theMetaObject->method(nMetathodIndex).name();
149 if (actionname.compare(oneMethod, Qt::CaseSensitivity::CaseInsensitive)==0)
150 {
151 method = oneMethod;
152 break;
153 }
154 }
155 if (!method.isEmpty())
156 {
157 auto token=request.getHeader("X - Token");
158 //判断token是否是可用。
159 auto v = QMetaObject::invokeMethod(result, method.data(), Qt::DirectConnection,
160 Q_ARG(HttpRequest &, request),
161 Q_ARG(HttpResponse &, response));
162 if (!v)
163 qDebug() << method.data()<<" method invokeMethod is error!";
164 }
165 else
166 {
167 //不存在的方法。
168 auto v = QMetaObject::invokeMethod(result, "ApiResult", Qt::DirectConnection,
169 Q_ARG(const QString&, actionname+" action not found !"),
170 Q_ARG(int, 1),
171 Q_ARG(HttpRequest &, request),
172 Q_ARG(HttpResponse &, response));
173 if (!v)
174 qDebug() << " service method invokeMethod is error!";
175 }
176 delete result;
177 }
178 else
179 {
180 qDebug() << "UnknownType service method invokeMethod is error!";
181 }
182
183 qDebug("RequestMapper: finished request");
184
185 }
特别说明:当有新的控制器添加的时候,只需要在requestmapper.cpp
文件注册控制器即可。
在BitPos 项目 main.cpp 源码修改如下:
1 #include "BitPos.h"
2 #include <QtWidgets/QApplication>
3 #include <QWebEngineView>
4 #include <httpserver/httplistener.h>
5 #include <logging/filelogger.h>
6 #include <qdir.h>
7 #include "src/requestmapper.h"
8 using namespace stefanfrings;
9
10 /** Search the configuration file */
11 QString searchConfigFile()
12 {
13 QString binDir = QCoreApplication::applicationDirPath();
14 QString appName = QCoreApplication::applicationName();
15 QString fileName(appName + ".ini");
16
17 QStringList searchList;
18 searchList.append(binDir);
19 searchList.append(binDir + "/etc");
20 searchList.append(binDir + "/../etc");
21 searchList.append(binDir + "/../../etc"); // for development without shadow build
22 searchList.append(binDir + "/../" + appName + "/etc"); // for development with shadow build
23 searchList.append(binDir + "/../../" + appName + "/etc"); // for development with shadow build
24 searchList.append(binDir + "/../../../" + appName + "/etc"); // for development with shadow build
25 searchList.append(binDir + "/../../../../" + appName + "/etc"); // for development with shadow build
26 searchList.append(binDir + "/../../../../../" + appName + "/etc"); // for development with shadow build
27 searchList.append(QDir::rootPath() + "etc/opt");
28 searchList.append(QDir::rootPath() + "etc");
29
30 foreach(QString dir, searchList)
31 {
32 QFile file(dir + "/" + fileName);
33 if (file.exists())
34 {
35 // found
36 fileName = QDir(file.fileName()).canonicalPath();
37 qDebug("Using config file %s", qPrintable(fileName));
38 return fileName;
39 }
40 }
41
42 // not found
43 foreach(QString dir, searchList)
44 {
45 qWarning("%s/%s not found", qPrintable(dir), qPrintable(fileName));
46 }
47 qFatal("Cannot find config file %s", qPrintable(fileName));
48 }
49
50 int main(int argc, char* argv[])
51 {
52 QApplication a(argc, argv);
53 // Find the configuration file
54 QString configFileName = searchConfigFile();
55
56 // Configure and start the TCP listener
57 QSettings* listenerSettings = new QSettings(configFileName, QSettings::IniFormat, &a);
58 listenerSettings->beginGroup("listener");
59 new HttpListener(listenerSettings, new RequestMapper(&a), &a);
60 //浏览器
61 QWebEngineView view;
62 //设置访问地址
63 view.setUrl(QUrl("http://localhost:5050/vue-element-admin/api/Login?user_code=333&password=3445"));
64 //显示浏览器窗口。
65 view.show();
66 return a.exec();
67 }
文件目录如下图:
按f5运行,即可看到接口返回(也可以用浏览器看接口返回),如下图:
如果后面添加更多的模块的接口,只需要按模板添加控制器,按操作在控制器内添加方法即可。
至此,qt 接口开发演示完毕,下一节讲解如何使用postman 进行接口调试。