Fun with PyQt5+CMake+C++

本文相关代码GitCode地址

这个项目与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动作统计得到。整体效果如图:

五彩斑斓的黑:Fun with PyQt5+CMake+C++-LMLPHP

我们先设计一个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下面的工具链环境设置快捷方式。
五彩斑斓的黑:Fun with PyQt5+CMake+C++-LMLPHP

在终端里切换到刚才的build目录,运行:

msbuild -property:Configuration=Release qthello.sln

就可以在Release文件中得到可执行文件。

结论

  1. miniconda3安装PyQt5之后带了全套的Qt环境,可以直接拿来开发Qt5程序;
  2. 没有debug版本的库和动态链接文件;
  3. "C:\ProgramData\miniconda3\Library\bin"没加到path的话,需要多拷贝好几个动态链接库文件,就是icuin58.dll zlib.dll icuuc58.dll zstd.dll icudt58.dll libpng16.dll这一堆,如果运行不正确,还有可能要增加其他的dll文件;
  4. cmake真香。
05-07 21:59