Fun Pain Fun with PyQt5+CMake+C++
这个项目与PyQt5只有半毛钱关系。事情是这样发生的。当时,我在一个新电脑上干活,装了miniconda,装了PyQt5,干着干着突然要整一个Qt5。我想也挺好,据说C++ 17里面lambda写得飞起,好久没有体验。
看了下这台windows笔记本的软件:
- CMake 3.25.2
- Windows SDK 10.0
没装QT,想到网上下,没网络。仔细想想,我miniconda的PyQt5不是好着的吗?
- miniconda with PyQt5
好吧,删繁就简三秋树,什么什么二月花,全语言战神不惧断网……乙方就是我二大爷,五彩斑斓的黑那是小Case!
Objective
我们先设置一个小目标,报表:按钮被点击的次数,采用LCDNumber来显示;数据:通过按钮的clicked动作统计得到。整体效果如图:
我们先设计一个QMainWindow的子类,CountingMainWindow,具体UI我们通过designer来设计。按钮上的图标、程序的图标,我们都定义到资源文件中。这样一个程序就具备全部的特性。
CMakeLists.txt
首先,从CMakeLists.txt开始。
- 文件开始规定cmake的版本;
- 接下来工程名称;
- 接下来是编译器支持的C++版本;
- 接下来五个开关量,分别的意思是自动化moc文件、自动化资源文件、自动化ui文件、去掉终端黑框整windows程序、包含当前目录(这是考虑uic自动生成的头文件);
- 接下来是最重要的部分:CMAKE_PREFIX_PATH,设置为miniconda3的安装目录下的Library文件,仔细看看,这里面的bin对应了QT的dll文件和各工具(包括designer,uic,rcc这些),cmake文件也在这个目录下的lib/cmake中;
- 全新的Qt5连接方式,find_package全能工具;
- 可执行文件的包含内容:add_executable
- target_link_libraries:链接库文件
- 后面if…endif() 拷贝dll文件,插件的dll文件
- 最后的foreach……endforeach(),拷贝额外的dll文件
cmake_minimum_required(VERSION 3.25)
project(qthello)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_WIN32_EXECUTABLE ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_PREFIX_PATH "C:\\ProgramData\\miniconda3\\Library")
find_package(Qt5 COMPONENTS
Core
Gui
Widgets
REQUIRED)
add_executable(${PROJECT_NAME} main.cpp countingmainwindow.cpp countingmainwindow.h countingmainwindow.ui resources.qrc)
target_link_libraries(${PROJECT_NAME}
Qt5::Core
Qt5::Gui
Qt5::Widgets
)
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)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${QT_INSTALL_PATH}/bin/Qt5${QT_LIB}_conda.dll"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>")
endforeach (QT_LIB)
endif ()
foreach (LIB_DLL icuin58.dll zlib.dll icuuc58.dll zstd.dll icudt58.dll libpng16.dll)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${QT_INSTALL_PATH}/bin/${LIB_DLL}"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>")
endforeach (LIB_DLL)
整个这里面,比较关键的部分就是这里的"${QT_INSTALL_PATH}/bin/Qt5${QT_LIB}_conda.dll"
,因为miniconda的dll文件名称增加了_conda
,库文件依靠提供的CMake配置文件自动处理过,但是动态链接库需要自己确定。
foreach (QT_LIB Core Gui Widgets)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${QT_INSTALL_PATH}/bin/Qt5${QT_LIB}_conda.dll"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>")
endforeach (QT_LIB)
Actual application
C++ source
首先是main.cpp
#include <QApplication>
#include <QPushButton>
#include <QStyleFactory>
#include <countingmainwindow.h>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
a.setStyle(QStyleFactory::create("Fusion"));
my_ui::CountingMainWindow window;
window.show();
return QApplication::exec();
}
这里,所有的工作都在my_ui::CountingMainWindow
这个类中完成。这个类的接口定义在countingmainwindow.h中。
//
// Created by User on 2023/5/7.
//
#ifndef QTHELLO_COUNTINGMAINWINDOW_H
#define QTHELLO_COUNTINGMAINWINDOW_H
#include <QMainWindow>
namespace my_ui {
QT_BEGIN_NAMESPACE
namespace Ui { class CountingMainWindow; }
QT_END_NAMESPACE
class CountingMainWindow : public QMainWindow {
Q_OBJECT
public:
explicit CountingMainWindow(QWidget *parent = nullptr);
~CountingMainWindow() override;
private:
Ui::CountingMainWindow *ui;
};
} // my_ui
#endif //QTHELLO_COUNTINGMAINWINDOW_H
这个类的接口中最重要的就是,那个私有变量Ui::CountingMainWindow *ui
,这个类将由ui文件生成。
对应的cpp文件中,在构造函数中new Ui::CountingMainWindow
来初始化私有变量,然后调用ui->setupUi(this);
设置界面元素、布局之类。接下来就是引用资源,设置图标;链接点击事件和LCDNumber溢出归零。
//
// Created by User on 2023/5/7.
//
// You may need to build the project (run Qt uic code generator) to get "ui_CountingMainWindow.h" resolved
#include "countingmainwindow.h"
#include "ui_CountingMainWindow.h"
namespace my_ui {
CountingMainWindow::CountingMainWindow(QWidget *parent) :
QMainWindow(parent), ui(new Ui::CountingMainWindow) {
ui->setupUi(this);
setWindowIcon(QIcon(":imgs/icon.png"));
ui->pushButton->setIcon(QIcon(":imgs/click.png"));
ui->pushButton->setIconSize(QSize(36, 36));
connect(ui->pushButton, &QPushButton::clicked, [=](bool checked){
ui->lcdNumber->display(ui->lcdNumber->intValue()+1);
});
connect(ui->lcdNumber, &QLCDNumber::overflow, [=](){
ui->lcdNumber->display(0);
});
}
CountingMainWindow::~CountingMainWindow() {
delete ui;
}
} // my_ui
Resource
资源文件内容简单。
<RCC>
<qresource>
<file>imgs/icon.png</file>
<file>imgs/click.png</file>
</qresource>
</RCC>
ui definition
UI文件由Designer生成。
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>my_ui::CountingMainWindow</class>
<widget class="QMainWindow" name="my_ui::CountingMainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>167</height>
</rect>
</property>
<property name="windowTitle">
<string>CountingMainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pushButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>24</pointsize>
</font>
</property>
<property name="text">
<string>Clicked:</string>
</property>
</widget>
</item>
<item>
<widget class="QLCDNumber" name="lcdNumber">
<property name="font">
<font>
<pointsize>24</pointsize>
</font>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>23</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
Building process
所有的文件都已经介绍完成,代码在gitcode可以克隆。
git clone https://gitcode.net/withstand/qthello.git
确保cmake命令可以访问的情况下,在qthello目录下运行如下指令。
mkdir build
cd build
cmake ..
目录下就会生成qthello.sln文件等其他一堆工程文件。运行Visual Studio 20xx下面的工具链环境设置快捷方式。
在终端里切换到刚才的build目录,运行:
msbuild -property:Configuration=Release qthello.sln
就可以在Release文件中得到可执行文件。
结论
- miniconda3安装PyQt5之后带了全套的Qt环境,可以直接拿来开发Qt5程序;
- 没有debug版本的库和动态链接文件;
- "C:\ProgramData\miniconda3\Library\bin"没加到path的话,需要多拷贝好几个动态链接库文件,就是icuin58.dll zlib.dll icuuc58.dll zstd.dll icudt58.dll libpng16.dll这一堆,如果运行不正确,还有可能要增加其他的dll文件;
- cmake真香。