Qt实现跨平台IM客户端(国产化)

整体效果

界面部分

主界面
主界面
独立弹窗,方便同事处理回复多个会话
独立窗口
会话搜索
在这里插入图片描述

消息搜索
在这里插入图片描述
发送各种类型的消息
在这里插入图片描述
暗黑皮肤
在这里插入图片描述
消息搜索
在这里插入图片描述

功能实现
  1. qt5.15.2 + cmake 纯Qt Widget实现,代码逻辑清晰,可跨平台在Windows,Mac,Linux以及国产化操作系统上运行。
  2. 实现文字,表情,图片,文件,图文混排消息,合并消息,视频消息,文件,
  3. 会话搜索,基于sqlite数据库实现单人会话以及群聊会话搜索
  4. 消息搜索
  5. 皮肤切换
  6. 独立窗口
  7. 组织架构显示
  8. 应用Tab页
  9. 内置浏览器
  10. 低内存占比及CPU,没有Electron实现客户端的性能困扰。

代码实现

工程代码结构

在这里插入图片描述
总代码行数超过25000+,采用MVC架构进行开发,代码结构以及逻辑清晰。可方便对接第三方IMSDK。

发送消息

获取输入框文本消息

void KInputTextEdit::getInputMsgInfoList(KMsgInfoList& msgInfoList) {
    QTextDocument*       textDocument = document();
    QTextBlock           currentBlock = textDocument->begin();
    QTextBlock::iterator it;
    QSet<int64_t>        curAtUsrId;

    KMsgInfo msgInfo;
    msgInfo.cId         = KUtils::genRequestId();
    msgInfo.summaryText = "";
    msgInfo.sessId = parent()->property("sessId").toLongLong() == 0 ? KApp->getMainWndController()->getCurrentUISessId()
                                                                    : parent()->property("sessId").toLongLong();
    msgInfo.msgType       = EMsgType::NORMAL;
    msgInfo.msgTime       = KUtils::getCurTimeMilliSeconds();
    msgInfo.senderId      = KApp->getSelfUserId();
    bool       isPreReply = false;
    bool       isPreImage = false;
    bool       isPreEmoji = false;
    QJsonArray elementArray;
    while (currentBlock.isValid()) {
        for (it = currentBlock.begin(); !(it.atEnd()); ++it) {
            QTextFragment currentFragment = it.fragment();
            if (!currentFragment.isValid()) {
                continue;
            }
            QTextFormat currentFormat = currentFragment.charFormat();
            if (currentFormat.isImageFormat()) {

                QString emojiKey = currentFormat.property(KEmojiPropertyType).toString();
                if (!emojiKey.isEmpty() && KApp->m_mapEmojiKey2ImageStr.contains(emojiKey)) {
                    isPreReply = false;
                    isPreImage = false;
                    if (!isPreEmoji) {
                        std::shared_ptr<TxtMsgElement> txtSeg = nullptr;
                        if (msgInfo.elements.size() > 0) {
                            txtSeg = std::dynamic_pointer_cast<TxtMsgElement>(msgInfo.elements.back());
                            if (txtSeg) {
                                for (int i = 0; i < currentFragment.length(); ++i) {
                                    msgInfo.summaryText.append(QString("[%1]").arg(emojiKey));
                                    txtSeg->txt += QString("[%1]").arg(emojiKey);
                                }

                                isPreEmoji = true;
                            }
                        }
                        else {
                            auto textElement = std::make_shared<TxtMsgElement>();
                            for (int i = 0; i < currentFragment.length(); ++i) {
                                msgInfo.summaryText.append(QString("[%1]").arg(emojiKey));

                                textElement->isEmoji = true;
                                textElement->txt += QString("[%1]").arg(emojiKey);
                            }
                            msgInfo.elements.push_back(textElement);
                            isPreEmoji = true;
                        }
                    }
                    else {
                        auto txtSeg = std::dynamic_pointer_cast<TxtMsgElement>(msgInfo.elements.back());
                        if (txtSeg && txtSeg->isEmoji) {
                            for (int i = 0; i < currentFragment.length(); ++i) {
                                msgInfo.summaryText.append(QString("[%1]").arg(emojiKey));
                                txtSeg->txt += QString("[%1]").arg(emojiKey);
                            }

                            isPreEmoji = true;
                        }
                    }
                }
                else {
                    QString   filename = currentFormat.property(KImagePropertyType).toString();
                    auto      img      = KUtils::genImageFromFileContent(filename);
                    kFileInfo fileInfo;
                    fileInfo.filePath  = filename;
                    fileInfo.fileSize  = QFileInfo(filename).size();
                    fileInfo.hasDown   = true;
                    fileInfo.imgWidth  = img.width();
                    fileInfo.imgHeight = img.height();
                    fileInfo.reqId     = KUtils::genRequestId();
                    fileInfo.sessId    = msgInfo.sessId;
                    msgInfo.fileInfoList.push_back(fileInfo);

                    for (int i = 0; i < currentFragment.length(); ++i) {

                        auto imgElement      = std::make_shared<ImageFileMsgElement>();
                        imgElement->filePath = filename;
                        imgElement->height   = img.height();
                        imgElement->width    = img.width();
                        imgElement->size     = QFileInfo(filename).size();
                        msgInfo.elements.push_back(imgElement);
                        isPreImage = true;
                        isPreEmoji = false;
                    }
                }
            }
            else if (currentFormat.isCharFormat()) {
                if (currentFormat.objectType() == KAtUserTextObject) {
                    qint64  curUserId = currentFormat.property(KAtUserUserIdPropertyType).toLongLong();
                    QString name      = currentFormat.property(KAtUserNamePropertyType).toString();

                    msgInfo.m_atUserIds.insert(curUserId);
                    msgInfo.summaryText.append(name);

                    //高亮
                    KMsgAtUserData atData;
                    int            length  = name.length();
                    int            begin   = msgInfo.summaryText.indexOf(name);
                    atData.atUserId        = curUserId;
                    atData.atUserName      = name.right(length - 1);
                    atData.isOutSideAtUser = false;
                    atData.type            = 1;
                    atData.spanBegin       = begin;
                    atData.spanEnd         = begin + length;
                    msgInfo.atUserDataList.push_back(atData);
                    auto textElement = std::make_shared<TxtMsgElement>();
                    textElement->txt = name;
                    msgInfo.elements.push_back(textElement);

                    isPreReply = false;
                    isPreImage = false;
                    isPreEmoji = false;
                }
                else if (currentFormat.objectType() == KReplyMsgTextObject) {
                    msgInfo.msgType      = EMsgType::REPLY;
                    auto refMsg          = currentFormat.property(KReplyMsgPropertyType).value<KMsgInfo>();
                    msgInfo.m_refMsgInfo = std::make_shared<KMsgInfo>(refMsg);
                    isPreReply           = true;
                    isPreImage           = false;
                    isPreEmoji           = false;
                }
                else {
                    //处理换行
                    QString txt = currentFragment.text();
                    QChar * ub = txt.data(), *uc = ub, *ue = uc + txt.size();
                    for (; uc != ue; ++uc) {
                        switch (uc->unicode()) {
                        case 0xfdd0:  // QTextBeginningOfFrame
                        case 0xfdd1:  // QTextEndOfFrame
                        case QChar::ParagraphSeparator:
                        case QChar::LineSeparator:
                            *uc = QLatin1Char('\n');
                            break;
                        case QChar::ObjectReplacementCharacter:
                        case QChar::Nbsp:
                            *uc = QLatin1Char(' ');
                            break;
                        }
                    }

                    msgInfo.summaryText.append(txt);

                    std::shared_ptr<TxtMsgElement> txtSeg = nullptr;
                    if (msgInfo.elements.size() > 0) {
                        txtSeg = std::dynamic_pointer_cast<TxtMsgElement>(msgInfo.elements.back());
                    }
                    if (!txtSeg) {
                        auto txtE = std::make_shared<TxtMsgElement>();
                        txtE->txt = txt;
                        msgInfo.elements.push_back(txtE);
                    }
                    else if (txtSeg) {
                        txtSeg->txt += txt;
                    }
                    isPreReply = false;
                    isPreImage = false;
                    isPreEmoji = false;
                }
            }
        }
        if (currentBlock.next() != textDocument->end()) {
            if (!isPreReply && !isPreImage) {
                msgInfo.summaryText.append(QString('\n'));
                TxtMsgElement textElement;
                textElement.txt = "\n";
                msgInfo.elements.push_back(std::make_shared<TxtMsgElement>(textElement));
            }
        }

        currentBlock = currentBlock.next();
    }
    msgInfoList.msgInfoList.push_back(msgInfo);
}
消息渲染

富文本消息绘制

void KRichEdit::renderMsgInfo(KMsgInfo& msg, bool isJustCalHeight) {
    m_uiMsgInfo = &msg;
    if (msg.msgType == EMsgType::DOCS) {
        QTextCharFormat linkFormat;
        linkFormat.setAnchor(true);

        if (!msg.docInfo.linkUrl.isEmpty()) {
            linkFormat.setAnchorHref(msg.docInfo.linkUrl);
        }
        else {
            auto url = QJsonDocument::fromJson(msg.jsonContent.toUtf8()).object()["link_url"].toString();
            linkFormat.setAnchorHref(url);
        }
        linkFormat.setAnchorNames({ msg.summaryText });

        auto linkColor = KSkinUtils::instance()->getColorAccordTheme("KRichedit", "link",
                                                                     QApplication::palette().color(QPalette::Link));
        linkFormat.setForeground(linkColor);
        linkFormat.setUnderlineColor(linkColor);
        linkFormat.setForeground(linkColor);

        linkFormat.setToolTip(tr("open link"));
        linkFormat.setUnderlineStyle(QTextCharFormat::NoUnderline);

        textCursor().insertText(msg.summaryText, linkFormat);
    }
    else if (msg.msgType == EMsgType::GROUP_NOTICE) {
        QTextCharFormat linkFormat;
        linkFormat.setAnchor(true);

        if (!msg.docInfo.linkUrl.isEmpty()) {
            linkFormat.setAnchorHref(msg.docInfo.linkUrl);
        }
        else {
            auto url = QJsonDocument::fromJson(msg.jsonContent.toUtf8()).object()["file"].toObject()["link"].toString();
            linkFormat.setAnchorHref(url);
        }
        linkFormat.setAnchorNames({ tr("[Group announcement]")
                                    + QJsonDocument::fromJson(msg.jsonContent.toUtf8()).object()["name"].toString() });

        auto linkColor = KSkinUtils::instance()->getColorAccordTheme("KRichedit", "link",
                                                                     QApplication::palette().color(QPalette::Link));
        linkFormat.setForeground(linkColor);
        linkFormat.setUnderlineColor(linkColor);
        linkFormat.setForeground(linkColor);

        linkFormat.setToolTip(tr("open link"));
        linkFormat.setUnderlineStyle(QTextCharFormat::NoUnderline);

        textCursor().insertText(tr("[Group announcement]")
                                    + QJsonDocument::fromJson(msg.jsonContent.toUtf8()).object()["name"].toString(),
                                linkFormat);
    }
    else if (msg.msgType == EMsgType::GRAPH) {
        auto templateObj = QJsonDocument::fromJson(msg.jsonContent.toUtf8()).object();

        QString title = templateObj["title"].toString();
        auto    href  = templateObj["link_object"].toObject()["external_links"].toString();

        QTextCharFormat linkFormat;
        linkFormat.setAnchor(true);
        linkFormat.setAnchorHref(href);
        linkFormat.setAnchorNames({ title });

        auto linkColor = KSkinUtils::instance()->getColorAccordTheme("KRichedit", "link",
                                                                     QApplication::palette().color(QPalette::Link));
        linkFormat.setForeground(linkColor);
        linkFormat.setUnderlineColor(linkColor);
        linkFormat.setForeground(linkColor);

        linkFormat.setToolTip(tr("open link"));
        linkFormat.setUnderlineStyle(QTextCharFormat::NoUnderline);
        textCursor().insertText(title, linkFormat);
        textCursor().insertBlock();
        auto contentBodyStr = templateObj["summary"].toString();
        textCursor().insertText(contentBodyStr, QTextCharFormat());
    }
    else if (msg.msgType == EMsgType::TEMPLATE) {
        auto templateObj = QJsonDocument::fromJson(msg.jsonContent.toUtf8()).object();

        auto categray = templateObj["data"].toObject()["category"].toString();
        auto title    = templateObj["data"].toObject()["title"].toString();
        auto href     = templateObj["data"].toObject()["href"].toString();

        QString tempStr = QString("[%1]%2").arg(categray).arg(title);

        QTextCharFormat linkFormat;
        linkFormat.setAnchor(true);
        linkFormat.setAnchorHref(href);
        linkFormat.setAnchorNames({ tempStr });

        auto linkColor = KSkinUtils::instance()->getColorAccordTheme("KRichedit", "link",
                                                                     QApplication::palette().color(QPalette::Link));
        linkFormat.setForeground(linkColor);
        linkFormat.setUnderlineColor(linkColor);
        linkFormat.setForeground(linkColor);

        linkFormat.setToolTip(tr("open link"));
        linkFormat.setUnderlineStyle(QTextCharFormat::NoUnderline);
        textCursor().insertText(tempStr, linkFormat);
        auto contentBodyStr = templateObj["data"].toObject()["body"].toString();
        auto contentArray   = QJsonDocument::fromJson(contentBodyStr.toUtf8()).array();
        for (auto item : contentArray) {
            textCursor().insertBlock();
            auto itemObj     = item.toObject();
            auto key         = itemObj["key"].toString();
            auto value       = itemObj["value"].toString();
            auto templateStr = QString("%1: %2").arg(key).arg(value);
            textCursor().insertText(templateStr, QTextCharFormat());
        }
    }
    else if (msg.msgType == EMsgType::NOTIFY) {
        auto templateObj = QJsonDocument::fromJson(msg.jsonContent.toUtf8()).object();

        auto categray   = templateObj["category"].toString();
        auto title      = templateObj["title"].toString();
        auto href       = templateObj["href"].toObject()["url"].toString();
        auto contentStr = templateObj["body"].toObject()["content"].toString();

        QTextCharFormat linkFormat;
        linkFormat.setAnchor(true);
        linkFormat.setAnchorHref(href);
        linkFormat.setAnchorNames({ title });

        auto linkColor = KSkinUtils::instance()->getColorAccordTheme("KRichedit", "link",
                                                                     QApplication::palette().color(QPalette::Link));
        linkFormat.setForeground(linkColor);
        linkFormat.setUnderlineColor(linkColor);
        linkFormat.setForeground(linkColor);

        linkFormat.setToolTip(tr("open link"));
        linkFormat.setUnderlineStyle(QTextCharFormat::NoUnderline);
        textCursor().insertText(title, linkFormat);
        if (!contentStr.isEmpty()) {
            textCursor().insertBlock();
            textCursor().insertText(contentStr, QTextCharFormat());
        }
    }
    else if (msg.msgType == EMsgType::MsgTypeCard || msg.msgType == EMsgType::REPLY) {
        E_SEGMENT_TYPE elementType = kElementTypeUnknow;
        //渲染回复消息部分
        if (msg.msgType == EMsgType::REPLY && msg.m_refMsgInfo != nullptr) {
            _insertText(tr("Reply") + " " + msg.m_refMsgInfo->senderInfo.name + ":" + msg.m_refMsgInfo->summaryText,
                        true);
            elementType = kElementTypeReply;
            if (msg.elements.empty()) {
                textCursor().insertBlock();
                _insertText(msg.summaryText);
            }
        }

        //图文消息
        int imageIndex = 0;
        for (const auto& element : msg.elements) {
            if (element->segmentType == kElementTypeTxt) {
                auto txtSeg = std::dynamic_pointer_cast<TxtMsgElement>(element);
                auto txt    = txtSeg->txt;
                //插入换行
                if (elementType == kElementTypeImg) {
                    textCursor().insertBlock();
                }
                if (elementType == kElementTypeReply) {
                    textCursor().insertBlock();
                }
                _insertText(txt);
                elementType = kElementTypeTxt;
            }
            else if (element->segmentType == kElementTypeImg) {
                auto imgSeg   = std::dynamic_pointer_cast<ImageFileMsgElement>(element);
                auto showSize = KUtils::getImWndImageScaleSize(QSize(imgSeg->width, imgSeg->height));

                QImage   img;
                QScreen* screen = KApp->screenAt(QCursor::pos());
                img.setDevicePixelRatio(screen->devicePixelRatio());
                if (msg.fileInfoList.size() > imageIndex && QFileInfo::exists(msg.fileInfoList[imageIndex].filePath)) {
                    bool isTrue = KUtils::getImageFromCacheIfExist(msg.fileInfoList[imageIndex].filePath, img);
                    if (isTrue) {
                        img = img.scaled(showSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
                    }
                    if (img.isNull()) {
                        auto picStr = msg.fileInfoList[imageIndex].filePath;
                        img         = KUtils::genImageFromFileContent(picStr).scaled(showSize, Qt::IgnoreAspectRatio,
                                                                             Qt::SmoothTransformation);
                    }
                }
                else {
                    img = QImageReader(":/res/svg/loading05.svg").read();
                }

                // if (img.isNull())
                //    return;

                auto url = QUrl(KUtils::genRequestId());

                textCursor().document()->addResource(QTextDocument::ImageResource, url, QVariant(img));
                QTextImageFormat imageFormat;
                imageFormat.setAnchor(true);
                imageFormat.setAnchorHref(msg.docInfo.linkUrl);

                if (isJustCalHeight) {
                    imageFormat.setWidth(showSize.width());
                    imageFormat.setHeight(showSize.height());
                }
                else {
                    if (msg.fileInfoList.size() > imageIndex
                        && QFileInfo::exists(msg.fileInfoList[imageIndex].filePath)) {
                        imageFormat.setWidth(showSize.width());
                        imageFormat.setHeight(showSize.height());
                    }
                    else {
                        imageFormat.setWidth(img.width());
                        imageFormat.setHeight(img.height());
                    }
                }
                imageFormat.setAnchor(true);
                imageFormat.setAnchorHref(msg.fileInfoList[imageIndex].filePath);
                imageFormat.setName(url.toString());

                //插入换行
                if (elementType == kElementTypeTxt || elementType == kElementTypeImg
                    || elementType == kElementTypeReply) {
                    textCursor().insertBlock();
                }
                textCursor().insertImage(imageFormat);
                elementType = kElementTypeImg;
                imageIndex++;
            }
        }
    }

    if (!msg.searchKeyWord.isEmpty()) {
        m_isSearchMsgView = true;
    }
    resetTheme(m_isSelfSend);
}

交流

Qt&IM交流群:738042066
在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值