CPP已经结课,我提交的项目是Qt的入门项目,局域网聊天室LanChatRoom。
这个代码重构了很多遍。第一遍是照着明哥推荐到书,把代码抄了一遍。
但抄下来之后,各种问题,而且是清朝老代码。抄了一遍之后,对代码的业务逻辑已经有了一个大体的了解。
整个开发周期持续了一周,其实最开始两天就已经能跑了。但我觉得远古代码太丑陋了,所以我扔掉了了书本,选择重写。
重写的过程也是曲折的,而且每次都遇到新的或旧的问题。这些问题以及解决方案将在接下来的内容中分享给大家。希望可以帮助到有需要的同学。
IDE的选择
如果是跟我一样的新手的话,第一遍建议是去找书,抄项目代码。当然是理解地抄,而不是单纯的Ctrl+CV。
IDE建议开始选择Qt自带的QtCreater。因为这涉及到对ui的操作,以及信号槽机制。这对没有qt经验的同学来说很不友好。
但是QtCreater太丑陋了,而且代码补全也不好用。
所以我当时是已经熟悉了ui的各项操作之后,就转到clion里了。
熟悉信号槽之后,就可以考虑转到clion了。
而且clion默认配置的cmakelist文件也更加清晰。
我一开始是去书栈网找Qt的教程,但它们很少用到ui文件,而是直接用代码控制元素。实际上很多对象的属性和方法,是不需要去记的,直接用designer编辑ui文件就可以。
消息广播
消息广播利用的是传输层协议UDP。
消息广播需要将消息发送给同一局域网内的所有设备。如果使用TCP协议,则需要在每个设备上都建立连接,这会增加网络开销。而UDP协议是无连接的协议,只需要设置源IP地址、源端口、目标IP地址和目标端口即可发送数据,因此可以提高传输效率。
UDP协议也存在一些缺点,例如数据传输不保证可靠性。在局域网聊天室中,如果某个设备没有接收到消息,则不会影响其他设备的正常使用。
文件传输
文件传输用的是传输层协议TCP。
TCP具有可靠性、有序性和流量控制等特性,可以保证文件传输的顺利进行。
而且文件的发送也利用了qt的信号槽机制。触发readyread或byteswritten信号之后,才传输下一部分文件。能够正常进入事件循环。这样不会堵塞当前线程,实现类似多线程的效果。
如果用循环的话,会卡在循环内,无法进入事件循环,在传输结束之前,显示“无响应”。
文件收发有很多共有的部分,比如界面元素、进度条更新。这些共有的部分可以单独封装,交给子类实现。这属于软件设计模式中的策略模式。
QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
这是我每次重写都遇到的问题,需要指定代理方式,这可能跟我一直开着系统代理有关。
QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
connect(ui->selectFileButton, &QPushButton::clicked, this, &FileTransfer::selectFile);
connect(ui->transferFileButton, &QPushButton::clicked, this, &FileTransfer::transferFile);
connect(ui->cancelButton, &QPushButton::clicked, this, [=]() {
this->close();
});
connect
是qt特有的信号槽方法。使用的话需要继承QObject类,或者他的派生类。
selectFile
和transferFile
是纯虚函数,具体的策略在子类中实现。这里必须要用纯虚函数,交由子类实现。
cancelButton
触发“取消”事件,通过lambda
表达式实现。无论是接收还是发送,点击取消按钮的结果,都是关闭窗口,因此选择直接使用lambda
表达式简化代码。
文件图标
我是在Clion中构建的的cmake项目。
需要在构建目录中添加.rc
资源文件,并在.rc
资源文件中指定IDI_ICON1 ICON "resources/icons/beer.ico"
后面的路径是相对于构建目录的,如果不确定写相对还是写绝对,可以都试一试。
回车发送消息
实现原理就是重写eventFilter方法。
如果检测到键盘事件,先判断是不是回车,如果是回车就发送消息,如果是CTRL+回车,就插入换行符。
如果是粘贴事件,就尝试插入图片。插入图片有两种可能:
- 在粘贴板的元数据中
- 粘贴板存放的是文件地址url
把这两种情况都尝试一遍,如果能获取到图片,那就插入到输入框。
还创建了一个自定义工具类,实现一个静态工具方法imageToBase64
。用于将image对象转换为base64格式的字符串,嵌入到html
中。
构建多个可执行文件
一个项目构建多个可执行文件,而不是为每一个可执行文件创建新的项目。
这需要修改CmakeList文件,为每一个构建目标指定文件。
添加自定义目标add_custom_target
,允许一次编译所有可执行文件。
添加可执行文件add_executable
,允许一个项目编译生成多个可执行文件。
括号内,第一个参数LanChatRoom
是构建后的可执行文件名。
后面的所有参数,都是参与构建这个可执行文件的源代码文件,包括头文件、源文件、资源文件。之后可能还会导入更多。
条件编译
每次切换debug和release两种状态的时候,都增删代码,是不现实的。
这样项目中每一处需要修改的地方都需要修改。
在最开始的时候,我就是这么做的。把一些调试信息显示在ui上。比如,本来这个标签是显示文件路径的,我现在显示TcpSocket的错误信息。
前面也提了,这个代码重构了很多遍,每次重构的原因,都包括:这一编写的太丑了,乱七八糟的。
重构很多遍之后,才想起来软件设计师备考时学的:软件设计模式。
这种工科的概念,如果脱离实践,那么只是空洞的文字。就算接触到了,也需要重复很多遍才能把认识和实践联系起来。
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
上面这行代码是写在CmakeList中的,它的意思是,在预处理阶段,添加宏DEBUG
。
在代码中需要调试的地方,用#ifdef DEBUG
,进行条件编译。
窗口程序,不显示cmd
这需要在CmakeList中添加:
set(CMAKE_WIN32_EXECUTABLE TRUE)
否则会携带一个控制台窗口。
动态链接库
这一部分的作用是在编译时链接动态链接库。
并在编译后,把动态链接库.dll
复制到目标目录中。
target_link_libraries(LanChatRoom
Qt::Core
Qt::Gui
Qt::Widgets
Qt::Network
)
target_link_libraries(FileSender
Qt::Core
Qt::Gui
Qt::Widgets
Qt::Network
)
target_link_libraries(FileReceiver
Qt::Core
Qt::Gui
Qt::Widgets
Qt::Network
)
if (WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(DEBUG_SUFFIX)
if (MSVC AND CMAKE_BUILD_TYPE MATCHES "Debug")
set(DEBUG_SUFFIX "d")
endif ()
set(QT_INSTALL_PATH "${CMAKE_PREFIX_PATH}")
if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
endif ()
endif ()
if (EXISTS "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
endif ()
foreach (QT_LIB Core Gui Widgets Network)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${QT_INSTALL_PATH}/bin/Qt6${QT_LIB}${DEBUG_SUFFIX}.dll"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>")
endforeach (QT_LIB)
endif ()
实际上,可以只保留target_link_libraries
部分。
因为后面一大段的if
,作用是导入动态链接库文件,导入的这些仍然是不完整的。
最后需要用windeployqt
来补充依赖。用法就是windeployqt [文件名]
,比如:windeployqt lanchatroom.exe
。win环境下是大小写都可以的。
使用windeployqt
需要预先将所在目录添加到环境变量中,以我的电脑为例,windeployqt
在目录C:\Tools\Qt\6.6.1\mingw_64\bin
下。
也就是Qt版本文件夹下的mingw_64\bin
。
软件设计模式
我最开始接触,是前段时间准备软考的时候。
重写了这么多编,才对软件设计模式有稍微浅薄的理解。
这里面也用到了策略、状态等模式。
如果没有软件设计模式,那么整个项目将非常混乱。我觉得,从事软件工程,软件设计模式是必须的。
软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
下面是当时汇报的PPT,对其他组的作品也算是降维打击了,哈哈。
LanChatRoom - yuque.pptx