将程序界面和业务代码分离对提高开发效率和代码可读性是很有帮助的,主流的UI库如qt、wxWidgets和gtk都拥有自己的实现方法,下面分别演示一下。

1. 基于XRC的wxWidgets项目

1.1 编写XRC文件

wxWidgets支持通过xml格式定义UI界面并保存为XRC文件。通过XRC文件可以实现界面代码分离,但缺点是发布的程序相比非XRC版本体积会增加一些。

下面是官方案例1中的XRC文件示例,实现了一个对话框,将其保存为resource.xrc

<?xml version="1.0" ?>
<resource version="2.3.0.1">
  <object class="wxDialog" name="SimpleDialog">
    <title>Simple dialog</title>
    <object class="wxBoxSizer">
      <orient>wxVERTICAL</orient>
      <object class="sizeritem">
        <object class="wxTextCtrl" name="text"/>
        <option>1</option>
        <flag>wxALL|wxEXPAND</flag>
        <border>10</border>
      </object>
      <object class="sizeritem">
        <object class="wxBoxSizer">
          <object class="sizeritem">
            <object class="wxButton" name="clickme_btn">
              <label>Click</label>
            </object>
            <flag>wxRIGHT</flag>
            <border>10</border>
          </object>
          <object class="sizeritem">
            <object class="wxButton" name="wxID_OK">
              <label>OK</label>
            </object>
            <flag>wxLEFT</flag>
            <border>10</border>
          </object>
          <orient>wxHORIZONTAL</orient>
        </object>
        <flag>wxALL|wxALIGN_CENTRE</flag>
        <border>10</border>
      </object>
    </object>
  </object>
</resource>

wxWidgets诞生时间比较早,XRC相关的RAD工具有很多,可以参考官方百科2,选择自己喜欢的工具进行辅助开发。

1.2 编写工程文件

编写main.cpp文件如下所示,通过主窗口调用这个对话框。初始化XRC界面之后,通过宏XRCCTRL调用其中的窗口和控件类并连接事件处理和回调函数。

// --- main.cpp ---

#include <wx/wx.h>
#include <wx/xrc/xmlres.h>

extern void InitXmlResource();

class MyFrame : public wxFrame
{
public:
    MyFrame(const wxString &title);
    void OnClickme(wxCommandEvent &event);
    void OnShowDialog(wxCommandEvent &event);
};

class MyApp : public wxApp
{
public:
    bool OnInit() override;
};

wxIMPLEMENT_APP(MyApp);

bool MyApp::OnInit()
{
    wxXmlResource::Get()->InitAllHandlers();
    InitXmlResource();
    MyFrame *frame = new MyFrame("Main Window");
    frame->Show(true);
    return true;
}

MyFrame::MyFrame(const wxString &title)
    : wxFrame(nullptr, wxID_ANY, title, wxDefaultPosition, wxSize(400, 300))
{
    // Create a panel to put the button on
    wxPanel *panel = new wxPanel(this);
    // Create a button to show the dialog
    wxButton *showDialogBtn = new wxButton(panel, wxID_ANY, "Show Dialog", wxPoint(150, 120));
    showDialogBtn->Bind(wxEVT_BUTTON, &MyFrame::OnShowDialog, this);
}

void MyFrame::OnShowDialog(wxCommandEvent &event)
{
    // Create and show the dialog if not already created
    wxDialog m_dialog;
    if (!wxXmlResource::Get()->LoadDialog(&m_dialog, this, "SimpleDialog"))
    {
        wxMessageBox("Failed to load dialog");
        return;
    }
    wxTextCtrl *pText = XRCCTRL(m_dialog, "text", wxTextCtrl);
    if (pText)
        pText->ChangeValue("This is a simple dialog");

    XRCCTRL(m_dialog, "clickme_btn", wxButton)->Bind(wxEVT_BUTTON, &MyFrame::OnClickme, this);
    m_dialog.ShowModal();
}

void MyFrame::OnClickme(wxCommandEvent &event)
{
    wxMessageBox("clickme_btn is clicked");
}

按照官方文档3编写CMakeLists.txt文件如下,通过cmake调用wxrcresource.xrc编译为cpp文件,再内嵌入发布的可执行文件中,这样最终发布的程序就不需要带着.xrc文件,当然cpp源码也得做相应的适配才行。这里通过vcpkg工具链编译,相关参数通过vcpkg.cmake传入。

# --- CMakeLists.txt ---

cmake_minimum_required(VERSION 3.21)
set(PROJECT_NAME "wxrc_sample")
project(${PROJECT_NAME} LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)

include("${CMAKE_SOURCE_DIR}/vcpkg.cmake")

# find wxWidgets
find_package(wxWidgets CONFIG REQUIRED)

if(wxWidgets_USE_FILE) # not defined in CONFIG mode
  include(${wxWidgets_USE_FILE})
endif()

add_executable(${PROJECT_NAME} main.cpp)

# One or more XRC files containing your resources.
set(XRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/resource.xrc)

# Generate this file somewhere under the build directory.
set(RESOURCE_CPP ${CMAKE_CURRENT_BINARY_DIR}/resource.cpp)

# Not needed with the installed version, just use "wxrc".
find_program(WXRC
  NAMES wxrc
  PATHS "${wxWidgets_TOOLS_PATH}"
  REQUIRED)
message(STATUS "WXRC: ${WXRC}")

add_custom_command(
  OUTPUT ${RESOURCE_CPP}
  COMMAND ${WXRC} -c -o ${RESOURCE_CPP} ${XRC_FILES}
  DEPENDS ${XRC_FILES}
  DEPENDS ${WXRC}
  COMMENT "Compiling XRC resources"
)

target_sources(${PROJECT_NAME} PRIVATE ${RESOURCE_CPP})

if(MSVC)
  set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")

  # using /MT and /MTd link flags
  if(VCPKG_TARGET_TRIPLET MATCHES "static")
    set_property(TARGET ${PROJECT_NAME} PROPERTY
      MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
  endif()
endif()

if(wxWidgets_LIBRARIES EQUAL "")
  message(STATUS "wxWidgets_LIBRARIES is empty!")
  set(wxWidgets_LIBRARIES wx::core wx::base wx::xrc)
endif()

target_link_libraries(${PROJECT_NAME} ${wxWidgets_LIBRARIES})

# set installation directories
install(TARGETS ${PROJECT_NAME}
  BUNDLE DESTINATION bin
  RUNTIME DESTINATION bin
)

# install runtime dependencies
install(CODE [[
include("${CMAKE_SOURCE_DIR}/vcpkg.cmake")
file(GET_RUNTIME_DEPENDENCIES
    RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
    UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
    EXECUTABLES $<TARGET_FILE:wxrc_sample>
    DIRECTORIES "${wxWidgets_BIN_PATH}"
    PRE_EXCLUDE_REGEXES "system32"
    PRE_EXCLUDE_REGEXES "api-ms-*"
    POST_EXCLUDE_REGEXES "system32"
)
foreach(DEP_LIB ${RESOLVED_DEPS})
    file(INSTALL ${DEP_LIB} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
endforeach()
]])

因为cmake的生成器字符串$<TARGET_FILE:...>无法捕捉变量,只能写成$<TARGET_FILE:wxrc_sample>这种形式,对代码的复用性有一定影响。cmake的install(CODE)所定义的代码块无法捕捉外部变量,需要通过vcpkg.cmake传入搜索路径的变量。当然也可以直接写入绝对路径,但这样做代码的复用性就会变差,每次更新工具链都需要手动更改很不方便。

这里将相关变量分离到vcpkg.cmake文件中,只需要编译之前修改vcpkg.cmake文件中的VCPKG_PATHVCPKG_TARGET_TRIPLET变量指定vcpkg工具链即可。

# --- vcpkg.cmake ---

# set vcpkg path and triplet
set(VCPKG_PATH "D:/vcpkg/vcpkg")
set(VCPKG_TARGET_TRIPLET "x64-windows")

# set vcpkg bin path
set(VCPKG_BIN_PATH "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/bin")

# using vcpkg toolchain
set(CMAKE_TOOLCHAIN_FILE "${VCPKG_PATH}/scripts/buildsystems/vcpkg.cmake")

# set wxSidgets and NanoSVG search path
set(wxWidgets_DIR "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/share/wxWidgets")
set(NanoSVG_DIR "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/share/NanoSVG")

# set wxWidget tools path
set(wxWidgets_TOOLS_PATH "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/tools/wxwidgets")
set(wxWidgets_BIN_PATH "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/bin")

可能是版本问题,vcpkg的wxWidgets和依赖的NanoSVG的路径搜索不到,只能手动指定,在conda和msys2上没遇到这样的问题。

1.3 编译及发布

以下命令编译并发布release版本,发布时自动复制wxWidgets依赖的dll到可执行文件目录中。

cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/install

e3195996eb818c1926183d9ddff3dccf.png

以上是关于wxWidgets动态链接库版本编译发布的过程。如果不想带着一堆动态库一起发布的话,可以考虑使用静态库编译,这样可以直接生成单独的可执行程序而且省去了发布过程。wxWidgets许可证4较为宽松,使用静态库也不用担心商用闭源问题。

需要注意的是,vcpkg上使用x64-windows-static目标工具链进行静态库编译的话,需要将msvc链接器的运行库从/MD改为/MT,1.2中的CMakeLists.txt配置文件中已经包含了这个设置。

2. 基于glade的gtk4项目

2.1 使用glade编辑gtk界面

和wxWidgets上的XRC类似,gtk也支持xml定义UI界面,通过GtkBuilder5模块进行加载。目前已知的gtk的RAD工具主要是glade6,最新版本3.40.0只支持输出gtk3版本的ui文件,需要使用gtk4的命令行工具行将glade输出的ui文件转换成gtk4版本7

我们可以使用galde绘制一个gtk程序的界面,然后保存为gtk3_ui.glade

1161af1b5e9ca7975bbecb7b2676a7c0.png

使用gtk-builder-tool将gtk3版本的ui文件转换成gtk4版本。

gtk4-builder-tool simplify --3to4 gtk3_ui.glade >> gtk4_ui.glade

下面是转换之后的gtk4_ui.glade文件内容,一般情况下请不要手工编辑它。尽量用glade编辑界面,然后再通过gtk-builder-tool工具转换。

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <object id="window" class="GtkWindow">
    <property name="title">Grid</property>
    <child>
      <object id="grid" class="GtkGrid">
        <child>
          <object id="button1" class="GtkButton">
            <property name="label">Button 1</property>
            <layout>
              <property name="column">0</property>
              <property name="row">0</property>
            </layout>
          </object>
        </child>
        <child>
          <object id="button2" class="GtkButton">
            <property name="label">Button 2</property>
            <layout>
              <property name="column">1</property>
              <property name="row">0</property>
            </layout>
          </object>
        </child>
        <child>
          <object id="quit" class="GtkButton">
            <property name="label">Quit</property>
            <layout>
              <property name="column">0</property>
              <property name="row">1</property>
              <property name="column-span">2</property>
            </layout>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>

2.2 编写资源文件

gtk支持将ui文件以资源文件的形式内嵌到可执行文件中,需要编写资源文件,然后调用glib-compile-resources命令行工具将资源文件编译成C源文件。下面是资源文件resources.xml,同样是xml格式。除了ui文件,其他像文本文件和图片都可以通过资源文件定义的形式内嵌到程序中。这样做的好处显而易见,最终发布的程序不用带着一堆资源文件到处跑了。

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/gtk/glade">
    <file preprocess="xml-stripblanks">gtk4_ui.glade</file>
  </gresource>
</gresources>

2.3 编写工程文件

编写c源文件,定义如何调用ui文件。

// --- main.c ---

#include <gtk/gtk.h>
#include <glib/gstdio.h>

static void
print_hello(GtkWidget *widget,
            gpointer data)
{
  GtkWidget *parent_window = gtk_widget_get_parent(widget);
  GtkAlertDialog *dialog = gtk_alert_dialog_new("Hello World!");
  gtk_alert_dialog_set_modal(dialog, TRUE);
  gtk_alert_dialog_show(dialog, GTK_WINDOW(parent_window));
  // g_print ("Hello World\n");
}

static void
quit_cb(GtkWindow *window)
{
  gtk_window_close(window);
}

static void
activate(GtkApplication *app,
         gpointer user_data)
{
  /* Construct a GtkBuilder instance and load our UI description */
  GtkBuilder *builder = gtk_builder_new();
  gtk_builder_add_from_resource(builder, "/org/gtk/glade/gtk4_ui.glade", NULL);
  // gtk_builder_add_from_file (builder, "gtk4_ui.glade", NULL);

  /* Connect signal handlers to the constructed widgets. */
  GObject *window = gtk_builder_get_object(builder, "window");
  gtk_window_set_application(GTK_WINDOW(window), app);

  GObject *button = gtk_builder_get_object(builder, "button1");
  g_signal_connect(button, "clicked", G_CALLBACK(print_hello), NULL);

  button = gtk_builder_get_object(builder, "button2");
  g_signal_connect(button, "clicked", G_CALLBACK(print_hello), NULL);

  button = gtk_builder_get_object(builder, "quit");
  g_signal_connect_swapped(button, "clicked", G_CALLBACK(quit_cb), window);

  gtk_window_present(GTK_WINDOW(window));

  /* We do not need the builder any more */
  g_object_unref(builder);
}

int main(int argc,
         char *argv[])
{
#ifdef GTK_SRCDIR
  g_chdir(GTK_SRCDIR);
#endif

  GtkApplication *app = gtk_application_new("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);

  int status = g_application_run(G_APPLICATION(app), argc, argv);
  g_object_unref(app);

  return status;
}

CMakeLists.txt文件中自动调用glib-compile-resources将资源文件resources.xml编译成C文件。这里调用的是vcpkg。

# --- CMakeLists.txt ---

cmake_minimum_required(VERSION 3.21)
set(PROJECT_NAME glade_gtk4_sample)
project(${PROJECT_NAME} LANGUAGES C)
set(CMAKE_C_STANDARD 11)

set(SRC_LIST main.c)
include(${CMAKE_SOURCE_DIR}/vcpkg.cmake)

find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED gtk4)

include_directories(${GTK_INCLUDE_DIRS})
link_directories(${GTK_LIBRARY_DIRS})
find_program(GLIB_COMPILE_RESOURCES
    PATHS ${GLIB_TOOL_BIN_PATH}
    NAMES glib-compile-resources REQUIRED)
set(GRESOURCE_C resources.c)
set(GRESOURCE_XML resources.xml)
set(GRESOURCE_DEPENDENCIES gtk4_ui.glade)
add_custom_command(
    OUTPUT ${GRESOURCE_C}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMAND ${GLIB_COMPILE_RESOURCES}
    ARGS
    --generate-source
    --target=${CMAKE_CURRENT_BINARY_DIR}/${GRESOURCE_C}
    ${GRESOURCE_XML}
    VERBATIM
    MAIN_DEPENDENCY ${GRESOURCE_XML}
    DEPENDS ${GRESOURCE_DEPENDENCIES}
)
add_custom_target(
    glib-resource
    DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${GRESOURCE_C}
)

add_executable(${PROJECT_NAME}
    ${SRC_LIST}
    ${CMAKE_CURRENT_BINARY_DIR}/${GRESOURCE_C}
)

if(MSVC)
    set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS /entry:mainCRTStartup")
endif()

add_dependencies(${PROJECT_NAME} glib-resource)

target_link_libraries(${PROJECT_NAME} ${GTK_LIBRARIES})

include(GNUInstallDirs)
install(TARGETS ${PROJECT_NAME}
    BUNDLE DESTINATION bin
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

# install runtime dependencies
install(CODE [[
include(${CMAKE_SOURCE_DIR}/vcpkg.cmake)
file(GET_RUNTIME_DEPENDENCIES
    RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
    UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
    # path to the executable files
    EXECUTABLES $<TARGET_FILE:glade_gtk4_sample>
    # directories to search for library files
    DIRECTORIES ${IMPORT_BIN_PATH}
    PRE_EXCLUDE_REGEXES "system32"
    PRE_EXCLUDE_REGEXES "api-ms-*"
    POST_EXCLUDE_REGEXES "system32"
)
foreach(DEP_LIB ${RESOLVED_DEPS})
    file(INSTALL ${DEP_LIB} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
endforeach()
set(GTK_SEARCH_DIRS ${IMPORT_PATH})
set(GTK_SUB_DIRS "lib" "etc" "share")
set(SUB_DIR_PEN "gtk-4" "glib-2" "gdk" "themes" "icons")
foreach(SUB_DIR ${GTK_SUB_DIRS})
    foreach(PEN_DIR ${SUB_DIR_PEN})
        file(GLOB DIRECTORIES LIST_DIRECTORIES true RELATIVE "${GTK_SEARCH_DIRS}/${SUB_DIR}" "${GTK_SEARCH_DIRS}/${SUB_DIR}/*${PEN_DIR}*")
        foreach(DIR ${DIRECTORIES})
            if(IS_DIRECTORY "${GTK_SEARCH_DIRS}/${SUB_DIR}/${DIR}")
                file(INSTALL "${GTK_SEARCH_DIRS}/${SUB_DIR}/${DIR}" DESTINATION "${CMAKE_INSTALL_PREFIX}/${SUB_DIR}")
            endif()
        endforeach()
    endforeach()
endforeach()
]])

vcpkg调用定义在模块vcpkg.cmake中。

# --- vcpkg.cmake ---

# set vcpkg path and triplet
set(VCPKG_PATH "D:/vcpkg/vcpkg")
message(STATUS "VCPKG PATH: " ${VCPKG_PATH})
set(VCPKG_TARGET_TRIPLET "x64-windows")
message(STATUS "VCPKG TARGET TRIPLET: " ${VCPKG_TARGET_TRIPLET})

# using vcpkg toolchain
set(CMAKE_TOOLCHAIN_FILE "${VCPKG_PATH}/scripts/buildsystems/vcpkg.cmake")

set(PKG_CONFIG_EXECUTABLE "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/tools/pkgconf/pkgconf.exe")
set(ENV{PKG_CONFIG_PATH} "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/lib/pkgconfig")
message(STATUS "VCPKG PKGCONFIG PATH: " $ENV{PKG_CONFIG_PATH})

# set search path
set(IMPORT_PATH "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}")
message(STATUS "IMPORT PATH: " ${IMPORT_PATH})
set(IMPORT_BIN_PATH "${IMPORT_PATH}/bin")
message(STATUS "IMPORT BIN PATH: " ${IMPORT_BIN_PATH})
set(GLIB_TOOL_BIN_PATH "${IMPORT_PATH}/tools/glib")
message(STATUS "GLIB TOOL BIN PATH: " ${GLIB_TOOL_BIN_PATH})

当然,也可以定义一个mingw.cmake模块,使用msys2编译的时候将vcpkg.cmake替换成mingw.cmake模块就行了。

# --- mingw.cmake ---

set(CMAKE_C_FLAGS "-mwindows")

# set search path
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)
message(STATUS "GLIB RESOURCE TOOL: " ${GLIB_COMPILE_RESOURCES})
cmake_path(GET GLIB_COMPILE_RESOURCES PARENT_PATH IMPORT_BIN_PATH)
message(STATUS "IMPORT BIN PATH: " ${IMPORT_BIN_PATH})
cmake_path(GET IMPORT_BIN_PATH PARENT_PATH IMPORT_PATH)
message(STATUS "IMPORT PATH: " ${IMPORT_PATH})
set(GLIB_TOOL_BIN_PATH "${IMPORT_PATH}/bin")
message(STATUS "GLIB TOOL BIN PATH: " ${GLIB_TOOL_BIN_PATH})

这里吐槽下cmake,install(CODE)语句里面不仅没法捕捉外部变量,还不能调用if()...else()进行逻辑判断,甚至同样的语句段在终端命令行和vscode拓展cmake tool的表现截然不一……

2.4 编译及发布

mingw和vcpkg编译发布的命令行都一样,在终端上验证编译发布安装通过。使用vscode拓展cmake tool编译没报错,但发布安装的时候出错。

cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/install

再吐槽下gtk4,自从弃用了GtkMessageDialog之后,新的GtkAlertDialog也没做好,连最基本的对话框显示也没做好。gtk4的API不停变动,真心不推荐用在项目上。

7b44a0537d5e7e6d92c530edb27aa5e1.png

gtk采用的是LGPL许可证,而且基本只支持动态链接库版本,这意味着在Windows下发布gtk程序不得不带着一堆动态库。在Linux上则没有这个顾虑,因为gtk在Linux上的地位就如同win32 api之于Windows。

3. 基于QML的qt6项目

3.1 创建工程文件

实际上qt提供了两种定义UI的方式。一种是传统的widget方式,使用qtdesigner工具定义ui文件,再通过uic编译成cpp源文件,类似于手写cpp实现的界面,只不过多了rad工具包装转换而已。另一种是qt大力发展的QML,也就是本章节将要探讨的内容,使用专门的定义式的语言开发界面,通过QtQuick进行渲染实现。前者适合传统桌面应用开发,后者则拓展了移动端支持。关于QML语言本身,这里不作过多讨论,可以自行参考官方文档8

要快速创建一个QML项目,可以使用qt官方的IDE工具qtcreator创建一个QtQuick应用,根据向导提示快速完成项目创建。

dfa8e79b51f30a7a2f60835010012f04.png

此时的项目文件夹已经生成了qml文件、cpp入口文件和cmake的配置文件。

a0399e6bd5603e3ea9e49bd68b6678a5.png

cpp入口文件main.cpp内容是自动生成的。

// --- main.cpp ---

#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []()
        { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);
    engine.loadFromModule("qml_sample", "Main");
    return app.exec();
}

3.2 编辑QML界面文件

自动生成的qml文件Main.qml基本是一片空白,将之修改如下,加入标签显示和按钮,按钮按下时修改标签内容。

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 400
    height: 300
    title: "QML Example"
    // Centered layout
    Column {
        anchors.centerIn: parent
        spacing: 20
        // Label to display text
        Label {
            id: messageLabel
            text: "Hello, QML!"
            font.pixelSize: 20
            horizontalAlignment: Text.AlignHCenter
        }
        // Button to change label text
        Button {
            text: "Click Me"
            onClicked: {
                messageLabel.text = "Button Clicked!";
            }
        }
    }
}

3.3 修改cmake脚本

修改CMakeLists.txt文件内容,增加自动发布和复制依赖的脚本。qml程序发布9用的是qt_generate_deploy_qml_app_script脚本,与widget程序不同,需要注意。

# --- CMakeLists.txt ---

cmake_minimum_required(VERSION 3.21)
set(PROJECT_NAME qml_sample)
set(EXECUTE_NAME "${PROJECT_NAME}_app")
project(${PROJECT_NAME} VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/install")

include(${CMAKE_SOURCE_DIR}/vcpkg.cmake)

find_package(Qt6 REQUIRED COMPONENTS Quick)

qt_standard_project_setup(REQUIRES 6.8)
qt_add_executable(${EXECUTE_NAME}
    main.cpp
)
qt_add_qml_module(${EXECUTE_NAME}
    URI ${PROJECT_NAME}
    VERSION 1.0
    QML_FILES
    Main.qml
)

set_target_properties(${EXECUTE_NAME} PROPERTIES
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

target_link_libraries(${EXECUTE_NAME}
    PRIVATE Qt6::Quick
)

include(GNUInstallDirs)
install(TARGETS ${EXECUTE_NAME}
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

set(deploy_tool_options_arg "")

if(APPLE)
    set(deploy_tool_options_arg --hardened-runtime)
elseif(WIN32)
    set(deploy_tool_options_arg --no-compiler-runtime)
endif()

qt_generate_deploy_qml_app_script(
    TARGET ${EXECUTE_NAME}
    OUTPUT_SCRIPT deploy_script
    MACOS_BUNDLE_POST_BUILD
    NO_UNSUPPORTED_PLATFORM_ERROR
    DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
    DEPLOY_TOOL_OPTIONS ${deploy_tool_options_arg}
)

install(SCRIPT ${deploy_script})

install(CODE [[
include(${CMAKE_SOURCE_DIR}/vcpkg.cmake)
file(GET_RUNTIME_DEPENDENCIES
    RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
    UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
    # path to the executable files
    EXECUTABLES $<TARGET_FILE:qml_sample_app>
    # directories to search for library files
    DIRECTORIES ${IMPORT_BIN_PATH}
    PRE_EXCLUDE_REGEXES "system32"
    PRE_EXCLUDE_REGEXES "api-ms-*"
    POST_EXCLUDE_REGEXES "system32"
)
foreach(DEP_LIB ${RESOLVED_DEPS})
    file(INSTALL ${DEP_LIB} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
endforeach()
]])

这里为了脚本复用,引入了vcpkg.cmake文件,vcpkg路径统一在这里定义。


# --- vcpkg.cmake ---

# set vcpkg path and triplet
set(VCPKG_PATH "D:/vcpkg/vcpkg")
message(STATUS "VCPKG PATH: " ${VCPKG_PATH})
set(VCPKG_TARGET_TRIPLET "x64-windows")
message(STATUS "VCPKG TARGET TRIPLET: " ${VCPKG_TARGET_TRIPLET})

# using vcpkg toolchain
set(CMAKE_TOOLCHAIN_FILE "${VCPKG_PATH}/scripts/buildsystems/vcpkg.cmake")

set(Qt6_DIR "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}/share/qt6")

# set search path
set(IMPORT_PATH "${VCPKG_PATH}/installed/${VCPKG_TARGET_TRIPLET}")
message(STATUS "IMPORT PATH: " ${IMPORT_PATH})
set(IMPORT_BIN_PATH "${IMPORT_PATH}/bin")
message(STATUS "IMPORT BIN PATH: " ${IMPORT_BIN_PATH})
set(GLIB_TOOL_BIN_PATH "${IMPORT_PATH}/tools/glib")
message(STATUS "GLIB TOOL BIN PATH: " ${GLIB_TOOL_BIN_PATH})

3.4 编译及发布

编译和发布命令都是一样的。最好在终端命令行下运行发布,vscode的cmake tool拓展发布总是失败。

cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/install

dcb441254fe87fe7fe2b31fb1d063710.png

以上只考虑了qt6动态链接库版本的编译和发布。qt6采用的是LGPL许可证,如果没有采购商用版的话只能带着一堆动态库随着程序文件发布。

qt对qml的支持力度越来越大,传统的widget支持越来越不上心,有时间精力的话尽量转向qml。

4. 总结

qt6功能最完善,也是最庞大复杂的。更新较为激进,源码庞大复杂,编译更是折磨人,发布的程序体积也是三者中最大的。对编译器和编译工具链支持齐全,拥有自主开发的IDE和RAD工具,开发效率也是三者中最高的。

wxWidgets最传统,功能也相对完善,RAD工具也比较齐全。源码更新较为缓慢,api比较稳定,对跨平台和编译工具链支持也比较完善。最重要的,许可证较为宽松,避免了商业版权纠纷,这也许是这么多年屹立不倒的原因。

gtk4有点高不成低不就的感觉,依赖过多而且过于零碎不成系统。源码更新虽然比不上wxWidgets缓慢但让人感觉更多的是原地踏步,没有一个完整的规划。构建工具十分古老,实在难以适应现代大型项目开发需求。这倒不是我对pkgconfig和autoconf有什么偏见,而是这些工具本身就是针对Linux量身定制的,在跨平台上的开发效率实在令人捉急。