整体效果
界面部分
主界面
独立弹窗,方便同事处理回复多个会话
会话搜索
消息搜索
发送各种类型的消息
暗黑皮肤
消息搜索
功能实现
- qt5.15.2 + cmake 纯Qt Widget实现,代码逻辑清晰,可跨平台在Windows,Mac,Linux以及国产化操作系统上运行。
- 实现文字,表情,图片,文件,图文混排消息,合并消息,视频消息,文件,
- 会话搜索,基于sqlite数据库实现单人会话以及群聊会话搜索
- 消息搜索
- 皮肤切换
- 独立窗口
- 组织架构显示
- 应用Tab页
- 内置浏览器
- 低内存占比及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