chatwindow.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. #include "chatwindow.h"
  2. #include "appevent.h"
  3. #include "widgets/chatView/chat1/chatview.h"
  4. #include <QTableWidget>
  5. #include <QHeaderView>
  6. #include <QApplication>
  7. #include <QClipboard>
  8. #include <QMimeData>
  9. #include <QImageReader>
  10. #include <QStandardPaths>
  11. #include <QDir>
  12. #include <QFileInfo>
  13. #include <QMessageBox>
  14. #include <QScrollBar>
  15. #include <QSplitter>
  16. #include <QTextDocumentFragment>
  17. #include <QTextImageFormat>
  18. #include <QMenu>
  19. #include <QAction>
  20. #include <QUuid>
  21. // MultiFormatInputEdit 实现
  22. MultiFormatInputEdit::MultiFormatInputEdit(QWidget *parent)
  23. : QTextEdit(parent)
  24. {
  25. setAcceptDrops(true);
  26. setPlaceholderText("输入消息... (支持粘贴图片)");
  27. setMaximumHeight(120);
  28. setMinimumHeight(60);
  29. // 连接文本变化信号
  30. connect(this, &QTextEdit::textChanged, this, &MultiFormatInputEdit::onTextChanged);
  31. }
  32. QString MultiFormatInputEdit::getPlainText() const
  33. {
  34. // 获取纯文本,但保留图片占位符信息
  35. QString text = toPlainText().trimmed();
  36. return text;
  37. }
  38. QStringList MultiFormatInputEdit::getImagePaths() const
  39. {
  40. return m_imagePaths;
  41. }
  42. void MultiFormatInputEdit::insertImage(const QString &imagePath)
  43. {
  44. if (imagePath.isEmpty() || m_imagePaths.contains(imagePath))
  45. return;
  46. m_imagePaths.append(imagePath);
  47. insertImageIntoDocument(imagePath);
  48. emit imageInserted(imagePath);
  49. emit contentChanged();
  50. }
  51. void MultiFormatInputEdit::clearContent()
  52. {
  53. clear();
  54. m_imagePaths.clear();
  55. m_imageResourceMap.clear();
  56. document()->clear();
  57. emit contentChanged();
  58. }
  59. bool MultiFormatInputEdit::validateContent() const
  60. {
  61. QString text = getPlainText();
  62. // 检查是否有内容
  63. if (text.isEmpty() && m_imagePaths.isEmpty()) {
  64. return false;
  65. }
  66. // 检查文本长度
  67. if (text.length() > 5000) {
  68. return false;
  69. }
  70. // 检查图片文件是否存在
  71. for (const QString &imagePath : m_imagePaths) {
  72. if (!QFileInfo::exists(imagePath)) {
  73. return false;
  74. }
  75. }
  76. return true;
  77. }
  78. void MultiFormatInputEdit::removeImage(const QString &imagePath)
  79. {
  80. if (!m_imagePaths.contains(imagePath))
  81. return;
  82. m_imagePaths.removeAll(imagePath);
  83. // 从文档中移除图片资源
  84. if (m_imageResourceMap.contains(imagePath)) {
  85. QString resourceName = m_imageResourceMap[imagePath];
  86. document()->addResource(QTextDocument::ImageResource, QUrl(resourceName), QVariant());
  87. m_imageResourceMap.remove(imagePath);
  88. }
  89. emit imageRemoved(imagePath);
  90. emit contentChanged();
  91. }
  92. void MultiFormatInputEdit::dragEnterEvent(QDragEnterEvent *event)
  93. {
  94. if (event->mimeData()->hasImage() || event->mimeData()->hasUrls()) {
  95. event->acceptProposedAction();
  96. } else {
  97. QTextEdit::dragEnterEvent(event);
  98. }
  99. }
  100. void MultiFormatInputEdit::dropEvent(QDropEvent *event)
  101. {
  102. const QMimeData *mimeData = event->mimeData();
  103. if (mimeData->hasUrls()) {
  104. const QList<QUrl> urls = mimeData->urls();
  105. for (const QUrl &url : urls) {
  106. if (url.isLocalFile()) {
  107. QString filePath = url.toLocalFile();
  108. if (isImageFormat(filePath)) {
  109. insertImage(filePath);
  110. }
  111. }
  112. }
  113. event->acceptProposedAction();
  114. } else if (mimeData->hasImage()) {
  115. QPixmap pixmap = qvariant_cast<QPixmap>(mimeData->imageData());
  116. if (!pixmap.isNull()) {
  117. QString tempPath = saveImageToTemp(pixmap);
  118. if (!tempPath.isEmpty()) {
  119. insertImage(tempPath);
  120. }
  121. }
  122. event->acceptProposedAction();
  123. } else {
  124. QTextEdit::dropEvent(event);
  125. }
  126. }
  127. void MultiFormatInputEdit::keyPressEvent(QKeyEvent *event)
  128. {
  129. if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
  130. if (event->modifiers() & Qt::ControlModifier) {
  131. // Ctrl+Enter 发送消息
  132. emit sendRequested();
  133. return;
  134. } else {
  135. // 普通回车换行
  136. QTextEdit::keyPressEvent(event);
  137. }
  138. } else {
  139. QTextEdit::keyPressEvent(event);
  140. }
  141. }
  142. void MultiFormatInputEdit::insertFromMimeData(const QMimeData *source)
  143. {
  144. if (source->hasImage()) {
  145. QPixmap pixmap = qvariant_cast<QPixmap>(source->imageData());
  146. if (!pixmap.isNull()) {
  147. QString tempPath = saveImageToTemp(pixmap);
  148. if (!tempPath.isEmpty()) {
  149. insertImage(tempPath);
  150. }
  151. }
  152. } else {
  153. QTextEdit::insertFromMimeData(source);
  154. }
  155. }
  156. void MultiFormatInputEdit::contextMenuEvent(QContextMenuEvent *event)
  157. {
  158. QMenu *menu = createStandardContextMenu();
  159. // 检查是否点击在图片上
  160. QTextCursor cursor = cursorForPosition(event->pos());
  161. QTextCharFormat format = cursor.charFormat();
  162. if (format.isImageFormat()) {
  163. QTextImageFormat imageFormat = format.toImageFormat();
  164. QString imageName = imageFormat.name();
  165. // 查找对应的图片路径
  166. QString imagePath;
  167. for (auto it = m_imageResourceMap.begin(); it != m_imageResourceMap.end(); ++it) {
  168. if (it.value() == imageName) {
  169. imagePath = it.key();
  170. break;
  171. }
  172. }
  173. if (!imagePath.isEmpty()) {
  174. menu->addSeparator();
  175. QAction *removeAction = menu->addAction("删除图片");
  176. connect(removeAction, &QAction::triggered, [this, imagePath]() {
  177. removeImage(imagePath);
  178. });
  179. }
  180. }
  181. menu->exec(event->globalPos());
  182. delete menu;
  183. }
  184. void MultiFormatInputEdit::onTextChanged()
  185. {
  186. emit contentChanged();
  187. }
  188. void MultiFormatInputEdit::processImageData(const QByteArray &imageData, const QString &format)
  189. {
  190. QPixmap pixmap;
  191. if (pixmap.loadFromData(imageData, format.toUtf8().constData())) {
  192. QString tempPath = saveImageToTemp(pixmap, format);
  193. if (!tempPath.isEmpty()) {
  194. insertImage(tempPath);
  195. }
  196. }
  197. }
  198. bool MultiFormatInputEdit::isImageFormat(const QString &fileName) const
  199. {
  200. QStringList imageFormats = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"};
  201. QString suffix = QFileInfo(fileName).suffix().toLower();
  202. return imageFormats.contains(suffix);
  203. }
  204. QString MultiFormatInputEdit::saveImageToTemp(const QPixmap &pixmap, const QString &format)
  205. {
  206. QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
  207. QDir().mkpath(tempDir);
  208. QString fileName = QString("chat_image_%1.%2")
  209. .arg(QDateTime::currentMSecsSinceEpoch())
  210. .arg(format.toLower());
  211. QString filePath = QDir(tempDir).absoluteFilePath(fileName);
  212. if (pixmap.save(filePath, format.toUtf8().constData())) {
  213. return filePath;
  214. }
  215. return QString();
  216. }
  217. void MultiFormatInputEdit::insertImageIntoDocument(const QString &imagePath)
  218. {
  219. QPixmap pixmap(imagePath);
  220. if (pixmap.isNull())
  221. return;
  222. // 缩放图片以适应输入框
  223. int maxWidth = width() - 20; // 留出边距
  224. int maxHeight = 80; // 限制高度
  225. if (pixmap.width() > maxWidth || pixmap.height() > maxHeight) {
  226. pixmap = pixmap.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
  227. }
  228. // 生成唯一的资源名称
  229. QString resourceName = generateResourceName(imagePath);
  230. m_imageResourceMap[imagePath] = resourceName;
  231. // 将图片添加到文档资源
  232. document()->addResource(QTextDocument::ImageResource, QUrl(resourceName), pixmap);
  233. // 在光标位置插入图片
  234. QTextCursor cursor = textCursor();
  235. QTextImageFormat imageFormat;
  236. imageFormat.setName(resourceName);
  237. imageFormat.setWidth(pixmap.width());
  238. imageFormat.setHeight(pixmap.height());
  239. cursor.insertImage(imageFormat);
  240. cursor.insertText(" "); // 在图片后添加空格,方便编辑
  241. setTextCursor(cursor);
  242. }
  243. QString MultiFormatInputEdit::generateResourceName(const QString &imagePath)
  244. {
  245. return QString("image_%1_%2").arg(QUuid::createUuid().toString(QUuid::WithoutBraces))
  246. .arg(QFileInfo(imagePath).baseName());
  247. }
  248. // ChatWindow 实现
  249. ChatWindow::ChatWindow(WebSocketClient *webSocketClient, QWidget *parent)
  250. : QWidget(parent)
  251. , m_webSocketClient(webSocketClient)
  252. {
  253. setupUI();
  254. connectSignals();
  255. }
  256. void ChatWindow::setupUI()
  257. {
  258. setWindowTitle("聊天窗口");
  259. resize(800, 600);
  260. QVBoxLayout *mainLayout = new QVBoxLayout(this);
  261. // 消息显示区域
  262. m_messageView = new ChatView(this);
  263. mainLayout->addWidget(m_messageView, 1);
  264. setupInputArea();
  265. }
  266. void ChatWindow::setupInputArea()
  267. {
  268. // 输入区域容器
  269. QWidget *inputContainer = new QWidget;
  270. QVBoxLayout *inputLayout = new QVBoxLayout(inputContainer);
  271. inputLayout->setContentsMargins(5, 5, 5, 5);
  272. inputLayout->setSpacing(5);
  273. // 输入框
  274. m_inputEdit = new MultiFormatInputEdit(this);
  275. inputLayout->addWidget(m_inputEdit);
  276. // 按钮区域
  277. QHBoxLayout *buttonLayout = new QHBoxLayout;
  278. buttonLayout->addStretch();
  279. m_imageButton = new QPushButton("图片", this);
  280. m_fileButton = new QPushButton("文件", this);
  281. m_sendButton = new QPushButton("发送", this);
  282. buttonLayout->addWidget(m_imageButton);
  283. buttonLayout->addWidget(m_fileButton);
  284. buttonLayout->addWidget(m_sendButton);
  285. inputLayout->addLayout(buttonLayout);
  286. // 添加到主布局
  287. layout()->addWidget(inputContainer);
  288. // 隐藏文件按钮(暂未实现)
  289. m_fileButton->hide();
  290. }
  291. void ChatWindow::connectSignals()
  292. {
  293. // 输入框信号
  294. connect(m_inputEdit, &MultiFormatInputEdit::sendRequested,
  295. this, &ChatWindow::onSendClicked);
  296. connect(m_inputEdit, &MultiFormatInputEdit::contentChanged,
  297. this, &ChatWindow::onInputContentChanged);
  298. connect(m_inputEdit, &MultiFormatInputEdit::imageInserted,
  299. this, &ChatWindow::onImageInserted);
  300. // 按钮信号
  301. connect(m_sendButton, &QPushButton::clicked, this, &ChatWindow::onSendClicked);
  302. connect(m_imageButton, &QPushButton::clicked, this, &ChatWindow::onImageClicked);
  303. connect(m_fileButton, &QPushButton::clicked, this, &ChatWindow::onFileClicked);
  304. // WebSocket信号
  305. if (m_webSocketClient) {
  306. connect(m_webSocketClient, &WebSocketClient::messageReceived,
  307. [this](const ChatMessage &message) {
  308. if (message.hasImage()) {
  309. // 处理图片消息
  310. if (message.isLeft()) {
  311. m_messageView->addImageMessage(message.imagePath, message.avatar, message.senderName, true, message.text);
  312. } else {
  313. m_messageView->addImageMessage(message.imagePath, message.avatar, message.senderName, false, message.text);
  314. }
  315. } else {
  316. // 处理纯文本消息
  317. if (message.isLeft()) {
  318. m_messageView->addMessage(message.text, message.avatar, message.senderName, true);
  319. } else {
  320. m_messageView->addMessage(message.text, message.avatar, message.senderName, false);
  321. }
  322. }
  323. });
  324. connect(m_webSocketClient, &WebSocketClient::connectionStateChanged,
  325. [this](bool connected) {
  326. if (connected) {
  327. m_messageView->addSystemMessage("已连接到服务器");
  328. } else {
  329. m_messageView->addSystemMessage("与服务器断开连接");
  330. }
  331. });
  332. }
  333. }
  334. void ChatWindow::initWebsocket(const QString &roomId)
  335. {
  336. m_roomId = roomId;
  337. if (m_webSocketClient) {
  338. m_webSocketClient->connectToRoom(roomId.isEmpty() ? "default_room" : roomId);
  339. }
  340. }
  341. void ChatWindow::onSendClicked()
  342. {
  343. if (validateInput()) {
  344. sendMessage();
  345. }
  346. }
  347. void ChatWindow::onImageClicked()
  348. {
  349. QString fileName = QFileDialog::getOpenFileName(
  350. this,
  351. "选择图片",
  352. "",
  353. "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp)"
  354. );
  355. if (!fileName.isEmpty()) {
  356. m_inputEdit->insertImage(fileName);
  357. }
  358. }
  359. void ChatWindow::onFileClicked()
  360. {
  361. // 文件发送功能待实现
  362. QMessageBox::information(this, "提示", "文件发送功能暂未实现");
  363. }
  364. void ChatWindow::onRecallClicked()
  365. {
  366. // 撤回消息功能待实现
  367. }
  368. void ChatWindow::onInputContentChanged()
  369. {
  370. // 更新发送按钮状态
  371. m_sendButton->setEnabled(m_inputEdit->validateContent());
  372. }
  373. void ChatWindow::onImageInserted(const QString &imagePath)
  374. {
  375. Q_UNUSED(imagePath)
  376. // 图片已直接显示在输入框中,无需额外处理
  377. }
  378. void ChatWindow::sendMessage()
  379. {
  380. QString text = m_inputEdit->getPlainText();
  381. QStringList imagePaths = m_inputEdit->getImagePaths();
  382. // 发送文本消息
  383. if (!text.isEmpty()) {
  384. m_messageView->addMessage(text, "", "我", false);
  385. if (m_webSocketClient) {
  386. m_webSocketClient->sendMessage(text);
  387. }
  388. }
  389. // 发送图片消息
  390. for (const QString &imagePath : imagePaths) {
  391. sendImageMessage(imagePath, text);
  392. }
  393. // 清空输入框
  394. m_inputEdit->clearContent();
  395. }
  396. void ChatWindow::sendImageMessage(const QString &imagePath, const QString &text)
  397. {
  398. if (QFileInfo::exists(imagePath)) {
  399. m_messageView->addImageMessage(imagePath, "", "我", false, text);
  400. if (m_webSocketClient) {
  401. m_webSocketClient->sendImageMessage(imagePath, text);
  402. }
  403. }
  404. }
  405. bool ChatWindow::validateInput() const
  406. {
  407. return m_inputEdit->validateContent();
  408. }
  409. void ChatWindow::closeEvent(QCloseEvent *event)
  410. {
  411. event->ignore();
  412. emit windowCloseRequested();
  413. }