安卓/平台QtQuick+socket.io+C++怎样搭建一个聊天系统
扫描二维码
随时随地手机看文章
流程有些复杂,QML不支持调用很多常见的js引擎,我们可以利用webview来达到。不过在使用socket.io,发现必须要在安卓4.4版本或更高才行,这个不是安卓的问题,是最新版的Qt没有优化老版本的WebView,唉。 开发流程图: 从发送到回调: QML -> WEBVIEW中的socke.io -> node.js服务器 ->WEBVIEW -> (标注)C++ -> QML 标注:由于socket.io是异步的,QML调用JS允许有回调,但是不支持在WEBVIEW中的异步回调。因此用C++来转发。
#ifndef TQMLHELPER_H #define TQMLHELPER_H #include#include#includeclass TQmlHelper : public QObject { Q_OBJECT public: explicit TQmlHelper(QObject *parent = 0); // 代码规范: set开头 供QML调用 用来设置本类的QML对象 // do开头 给HTTP服务器调用 用来间接通信QML Q_INVOKABLE void setroot(QObject* obj); Q_INVOKABLE void setTXL(QObject* obj); //从聊天服务器获取好友 请求Qml刷新好友列表 void do_updataFlist(QByteArray& b); signals: public slots: private: QObject* root; //PageTongxunlv.qml: //Component.onCompleted: { // myapp.setTXL(gen); // console.log("cout:"+model.count); // model.append({name:"代码统计行数:"+model.count,tip:"代码生成的行"}); // } //PageTongxunlv中的跟节点 包含好友列表的一些成员函数 QObject* gen; }; #endif // TQMLHELPER_H
#include "tqmlhelper.h" #include#includeTQmlHelper::TQmlHelper(QObject *parent) : QObject(parent) { root = NULL; } void TQmlHelper::setroot(QObject* obj) { qDebug()<>>:"<< obj->objectName(); } void TQmlHelper::do_updataFlist(QByteArray& b) { qDebug()<<"do_updataFlist:"<<b; QString a = b; ; QMetaObject::invokeMethod(gen, "刷新好友列表", Qt::ConnectionType::BlockingQueuedConnection, Q_ARG(QVariant, a)); }
#include"myhttp.h" #includeextern TQmlHelper* tmphelp; Helloworldcontroller3::Helloworldcontroller3(QObject* parent):HttpRequestHandler(parent) { } #includevoid Helloworldcontroller3::service(HttpRequest &request, HttpResponse &response) { //允许跨域 response.setHeader("Access-Control-Allow-Origin", "*"); QByteArray path=request.getPath(); qDebug("RequestMapper: path=%s",path.data()); //登录状态 if (path=="/loging") { QByteArray a = request.getParameter("p1"); qDebug()<<a; if(a.toInt()==1) { qDebug()<<"loging ok"; }else{ qDebug()<<"loging erro"; } response.setStatus(200,"OK"); // HelloWorldController().service(request, response); } //获取好友列表的返回结果 else if (path=="/Flist") { QByteArray a = request.getParameter("p1"); tmphelp->do_updataFlist(a); response.write("ok",true); response.setStatus(200,"ok"); } else { response.setStatus(404,"oo!Not found"); QString s =QString::fromLocal8Bit("请请不要跨域哦"); QByteArray s2(s.toStdString().c_str()); response.write(s2,true); } qDebug("RequestMapper: finished request"); }
*****main.qml TQmlHelper* tmphelp; void RegFunc(QQmlApplicationEngine& engine) { tmphelp = new TQmlHelper(&engine); engine.rootContext()->setContextProperty("myapp", (QObject*)tmphelp); } #includeint main(int argc, char *argv[]) { QApplication::setApplicationName("Myapp"); QApplication::setOrganizationName("QtProject"); QApplication::setAttribute(Qt::AA_DisableHighDpiScaling); // qputenv("QT_SCALE_FACTOR", b); // system("su"); QApplication app(argc, argv); // MyWait* label = new MyWait(); // label->show(); // qDebug("********开始*********"); QSettings* listenerSettings= new QSettings("assets:/demo.ini",QSettings::IniFormat,&app); qDebug("config file loaded"); listenerSettings->beginGroup("listener"); // Start the HTTP server new HttpListener(listenerSettings, new Helloworldcontroller3(&app), &app); QQuickStyle::setStyle("Material"); QQmlApplicationEngine engine; RegFunc(engine); engine.load(QUrl(QLatin1String("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; // label->hide(); qDebug("********结束*********"); return app.exec(); }
由于如果用C++主动获取QML对象,可能存在QML对象尚未初始化问题,因此,在qml初始化后调用C++来设置QML对象指针。
import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.1 import QtQuick.Window 2.0 import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Material 2.0 import QtGraphicalEffects 1.0 import QtQuick.Particles 2.0 import "Ndk.js" as Ndk import "./code" import QtWebView 1.1 ApplicationWindow { id:root; visible: true; height: 480; width: 320; Component.onCompleted: { //把窗口对象传给Qt 之所以加载完毕后才传递 很多对象提前是无内存块 myapp.setroot(root); } property real pixelDensity: 4.46 property real multiplierH: root.height/480 //default multiplier, but can be changed by user property real multiplierW: root.width/320 //default multiplier, but can be changed by user function dpH(numbers) { return Math.round(numbers*((pixelDensity*25.4)/160)*multiplierH); } function dpW(numbers) { return Math.round(numbers*((pixelDensity*25.4)/160)*multiplierW); } function dpi2(px) { return Math.round(px*((Screen.pixelDensity*25.4)/160)) } /** * 将px值转换为dip或dp值,保证尺寸大小不变 * * @param pxValue * @param scale * (DisplayMetrics类中属性density) * @return */ function px2dip(pxValue) { var scale = myapp.getdensity(); return (pxValue / scale + 0.5); } property color accentcol:"red" property color backgroundcol:"white" property color foregroundcol:"#000000" property color primarycol:"blue" Material.accent:accentcol Material.background:backgroundcol Material.foreground:foregroundcol Material.primary: primarycol //消息框 MessageDialog { id:dlg; objectName: "xiaoxikuang"; width: Screen.width; height: Screen.hight; visible: false; title: "进度" icon: StandardIcon.Question text: "file.txt already exists. Replace?" detailedText: "To replace a file means that its existing contents will be lost. " + "The file that you are copying now will be copied over it instead." standardButtons: StandardButton.Yes | StandardButton.No } function messaggeBox(title,txt) { dlg.text = txt; dlg.title = title; dlg.visible = true; } //调试打印 Item { visible: true width:parent.width; height:dpH(100); z:100 // color:Ndk.SDColor_warning; ListView{ id:dbgtxt; width:parent.width; height:dpH(100); model: ListModel{ id:dbgmodel; ListElement{txt:"123"} } delegate: Label{ font.pixelSize: dpW(18); font.bold: true; color: Ndk.green_1 text:txt } displaced: Transition { NumberAnimation { properties: "x,y"; duration: 1000 } } move: Transition { NumberAnimation { properties: "x,y"; duration: 1000 } } } } function dbg(txt) { if(dbgmodel.count>=5) { dbgmodel.clear(); } dbgmodel.append({txt:txt}); } //////////////////////加载控制器////////////////////////// property alias mwebview: mywebview; property var txtprogress_var: 0 property var txtmainpage_var: 0 property var txtsubpage1_var: 0 Item { width: Screen.width; height: Screen.height visible: true z:100 id: bk; Rectangle{ id:zezao; opacity: 0.9; anchors.fill: parent; color:"#cccccc" Column{ width: parent.width; spacing: 2; //WebView加载进度 Label{ font.pixelSize: dpW(18); font.bold: true; color: Ndk.文字色偏白 id:txtprogress text:"WebKit加载进度:"+txtprogress_var } Rectangle{ anchors.margins: dpW(5); width: parent.width; height: dpH(20); color: Ndk.SDColor_success; radius: dpW(2) Rectangle{ radius: dpW(2) property var pass: 0 id:bartxt; width: parent.width/100*pass; height: dpH(20); color: Ndk.SDColor_info; Behavior on width {NumberAnimation{ easing.type: Easing.InOutBounce;duration: 2000}} } } //mainpage加载进度 Label{ id:txtmainpage; color: Ndk.文字色偏白 font.pixelSize: dpW(18); font.bold: true; text:"主页面加载进度:"+txtmainpage_var } Rectangle{ anchors.margins: dpW(5); width: parent.width; height: dpH(20); color: Ndk.SDColor_success; radius: dpW(2) Rectangle{ radius: dpW(2) property var pass: 0 id:barmainpage; width: parent.width/100*pass; height: dpH(20); color: Ndk.SDColor_info; Behavior on width {NumberAnimation{ easing.type: Easing.InOutBounce;duration: 2000}} } } //子页面加载进度 Label{ id:txtsubpage1; color: Ndk.文字色偏白 font.pixelSize: dpW(18); font.bold: true; text:"首页加载进度:"+txtmainpage_var } Rectangle{ anchors.margins: dpW(5); width: parent.width; height: dpH(20); color: Ndk.SDColor_success; radius: dpW(2) Rectangle{ radius: dpW(2) property var pass: 0 id:barsubpage1; width: parent.width/100*pass; height: dpH(20); color: Ndk.SDColor_info; Behavior on width {NumberAnimation{ easing.type: Easing.InOutBounce;duration: 2000}} } } } Behavior on opacity { NumberAnimation{ easing.type: Easing.InOutBounce;duration: 2000} } onOpacityChanged: { if(opacity==0) { bk.visible = false; bk.deleteLater(); dbg("onOpacityChanged"); } } } // 禁止事件穿透 MouseArea{ anchors.fill: parent; onPressed:{ mouse.accepted = true } // drag.target: root // root可拖动 } //外部调用刷新更新状态 并且判断是否全部加载成功 function changgepro(type,pro) { if(type == 0)//webview加载进度 { bartxt.pass = pro; txtprogress_var = pro; }else if(type == 1)//mainpage加载进度 { barmainpage.pass = pro; txtmainpage_var = pro; }else if(type == 2)//mainpage加载进度 { barsubpage1.pass = pro; txtsubpage1_var = pro; } if(txtprogress_var == 100 && txtmainpage_var == 100 && txtsubpage1_var ==100) { oninitOk(); } } //前台页面加载完毕后的行为 function oninitOk() { foot.visible = true; zezao.opacity = 0; benavShow = true; messaggeBox("状态","加载完毕"); // bk.visible = false; } } WebView{ //加载完毕 loading变成false visible: false; id:mywebview; objectName: "webview" url:"file:///android_asset/index.html"; onLoadProgressChanged: { // txtprogress.text="WebKit加载进度:"+mywebview.loadProgress; bk.changgepro(0,mywebview.loadProgress); } onLoadingChanged: { } } //页头 property alias roothd: hd header: ToolBar{ id:hd; states: [ State { name: "hide" PropertyChanges { target: hd;opacity:0;height:0;width:0; } PropertyChanges { target: lisetview;opacity:0;rotation:360;height:0; } } ] transitions: Transition { // Make the state changes smooth ParallelAnimation { NumberAnimation { duration: 500; properties: "opacity,x,contentY,height,width" } ColorAnimation { property: "color"; duration: 888 } NumberAnimation { duration: 888; properties: "rotation" } } } height:dpH(60); Text{ text:"mywebview.loadProgress+" anchors.centerIn: parent color: "white" font.pixelSize: dpW(18) } layer.enabled: true layer.effect: DropShadow { transparentBorder: true//绘制边框阴影 color: "#000000"; radius: dpH(15) id:drop; //cached: true; horizontalOffset: 0; verticalOffset: 0; samples: 16; smooth: true; } } function getdpistype() { console.log("SCALA: "+Screen.pixelDensity*25.4/160) console.log(Screen.pixelDensity) var curdpi = Screen.pixelDensity*25.4; var mydpi = curdpi.toFixed(0); console.log(mydpi); console.log("my dpi "+myapp.getdpi()) return "MYDPI+"+mydpi; if(mydpi>=480) { console.log("XXHDPI"); return "XXHDPI"; }else if(mydpi>=320) { console.log("XHDPI"); return "XHDPI"; }else if(mydpi>=240) { console.log("HDPI"); return "HDPI"; }else if(mydpi>=180) { console.log("MDPI"); return "MDPI"; }else { console.log("LDPI"); return "LDPI"; } } Loader{ id:mainpage; asynchronous: true anchors.fill: parent; sourceComponent: commapnpage; onProgressChanged: { bk.changgepro(1,progress*100); } } Component{ id:commapnpage; SwipeView{ state: "hide1" currentIndex: tabindex onCurrentIndexChanged: { console.log("onCurrentIndexChanged:"+currentIndex); tabindex = currentIndex;//导航栏的序号与这里同步,手动滑动触发这里 if(currentIndex==1) { tongxinlu.item.获取好友列表(); } } //首页 Page{ id:gouzhen; Loader{ id:shouye asynchronous: true//异步加载组件 anchors.fill: parent sourceComponent: Page_gouzhen{} onProgressChanged: { bk.changgepro(2,shouye.progress*100); } } } //通讯录 Page{ id:tongxun; Loader{ asynchronous: true id:tongxinlu; anchors.fill: parent; sourceComponent: PageTongxunlv{} } } } } //当前选中的导航序号 property var benavShow :false;//是否显示底部导航 property int tabindex: mainpage.item.currentIndex footer: Row{ visible: benavShow; id:foot; width:benavShow? parent.width:0; height:benavShow? width/5*0.75:0; Repeater{ id:rep delegate: NavNewDelegate{ width: benavShow?parent.width/5:0; height:benavShow?width*0.75:0; rotation:benavShow?0:Math.random()*360 id:navitem; Behavior on width {NumberAnimation {duration: 2000; easing.type: Easing.InOutQuad}} Behavior on height {NumberAnimation {duration: 2000; easing.type: Easing.InOutQuad}} Behavior on rotation {NumberAnimation {duration: 2000; easing.type: Easing.InOutQuad}} onClick: { dbg("Delegate传递过来的下标:"+index+tabindex); console.log("Delegate传递过来的下标:"+index+tabindex); } } model:NavNewModel{id:model1} } } Keys.enabled: true; Keys.onReleased: { console.log("key"+event.key); if(event.key==Qt.Key_Back) { console.log("back"); } } }
****关键代码 QML和C++以及webview中的js库交互的部分 import QtQuick 2.7 import QtQuick.Controls 1.4 as Old import QtQuick.Controls 2.0 import QtQuick.Layouts 1.0 import QtQuick.Layouts 1.1 import QtQuick.Window 2.0 import QtQuick.Dialogs 1.2 import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Material 2.0 import QtQuick.Controls.Universal 2.0 import QtGraphicalEffects 1.0 import QtQuick.Particles 2.0 import QtWebSockets 1.0 import "../" Item { property alias mview: view objectName: "FList"; anchors.fill: parent; id:gen; ListView{ id:view; anchors.fill: parent; delegate: ListTongxunlvDelegate{ } model: ListTongxunlvModel{ id:model; } Component.onCompleted: { myapp.setTXL(gen); console.log("cout:"+model.count); model.append({name:"代码统计行数:"+model.count,tip:"代码生成的行"}); } } //专门被C++调用的函数 function 刷新好友列表(x) { console.log("get c++ *****"+x) root.messaggeBox("刷新好友列表:",x); } function 获取好友列表() { mywebview.runJavaScript("获取好友列表()",function(result){ for(var x in result) { root.dbg(result[x]); } root.dbg(result); }); } }
**********HTML代码 使用裸页面只为加载JS功能 h
*****js代码 var msocket; mui.init({}) mui.ready(lod); function 登录成功(arg1) { // $.post("http://localhost:8080/loging?p1="+arg1, function(data){ // alert("Data Loaded: " + data.name); // },"json"); $.post("http://localhost:8080/loging?p1="+arg1); } function 发送到C加加(path,arg1) { // $.post("http://localhost:8080/loging?p1="+arg1, function(data){ // alert("Data Loaded: " + data.name); // },"json"); $.post("http://localhost:8080/"+path+"?p1="+arg1); } var isloging = false; function lod(){ alert("ok"); console.log("模拟器连接"); // msocket = io.connect('ws://10.0.2.2:8081', { 'reconnect': true });//模拟器访问局域网 msocket = io.connect('ws://192.168.0.101:8081', { 'reconnect': true });//模拟器访问局域网 msocket.on('connect',function(){ alert("连接成功;"); isloging = true; msocket.emit("denglu","admin","123",function(callbackdata){ alert("登录结果:"+callbackdata); }) 登录成功(1); }); //正在连接 msocket.on('connecting',function(){ alert("正在连接"); }); //连接超时 msocket.on('connect_timeout',function(){ console.log("connect_timeout"); }); //连接失败 msocket.on('connect_failed',function(){ alert("连接失败"); }); //错误发生 并且无法被其他事件类型所处理 msocket.on('error',function(data){ alert("错误发生 并且无法被其他事件类型所处理"); }); //重连失败 msocket.on('reconnect_failed',function(){ alert("reconnect_failed"); }); //成功重连 msocket.on('reconnect',function(TheNumber){ alert("reconnectOk"+TheNumber); }); //正在重连 msocket.on('reconnecting',function(TheNumber){ console.log("reconnecting"+TheNumber); }); } function Testfunc() { // alert("get") return 123; } //对应QML代码: //function 获取好友列表() // { // // mywebview.runJavaScript("获取好友列表()",function(result){ // for(var x in result) // { // root.dbg(result[x]); // } // root.dbg(result); // // // }); // } var mdata; function 获取好友列表() { if(isloging==false) { alert("暂未登录,无法拉取好友"); return ""; } alert("获取好友中"); msocket.emit("获取好友",/*"获取好友",*/function(data){ //传过来的是字符串 不需要转换了 交给QtQuick转换吧 一样 //不对 貌似qtquick可以直接解析对象 console.log(data) 发送到C加加("Flist",data); mdata = data; // console.log(JSON.parse(data)) }); //由于是异步的 因此可能先返回了 才接收到服务器发过来的数据 解决方式很多 这里用同步方式 alert("最后层返回"); return mdata;//此返回值无用 真正的返回值是由服务器转发过来经过 发送到C加加("Flist",data) 转发给QML }