C++11开始支持Lambda表达式,Lambda表达式除了可以作为匿名函数使用,还可以通过捕获列表访问环境中的变量。在UI编程中使用Lambda表达式替代传统的函数指针处理回调函数,不仅可以简化代码提高可读性,还可以减少参数传递提高灵活性。下面针对常用的C++ UI库尝试实现相关操作。

1. wxWidgets事件绑定

1.1 wxWidgets事件机制

wxWidgets有两套事件机制,一套是静态的事件表1,另一套是动态事件机制2。动态事件提供了两套API,分别是Connect()Bind()3,后者较新,前者已经淘汰。

实际上Bind()是原生支持Lambda表达式的,我们可以将官方文档的示例代码4改写如下。

// --- main.cpp ---

#include <wx/wx.h>

enum
{
    ID_Hello = 1
};

class MyFrame : public wxFrame
{
public:
    MyFrame()
        : wxFrame(nullptr, wxID_ANY, "Hello World")
    {
        wxMenu *menuFile = new wxMenu;
        menuFile->Append(ID_Hello, "&Hello...\tCtrl-H",
                         "Help string shown in status bar for this menu item");
        menuFile->AppendSeparator();
        menuFile->Append(wxID_EXIT);

        wxMenu *menuHelp = new wxMenu;
        menuHelp->Append(wxID_ABOUT);

        wxMenuBar *menuBar = new wxMenuBar;
        menuBar->Append(menuFile, "&File");
        menuBar->Append(menuHelp, "&Help");

        SetMenuBar(menuBar);

        CreateStatusBar();
        SetStatusText("Welcome to wxWidgets!");

        Bind(wxEVT_MENU, [](wxCommandEvent &event)
             { wxLogMessage("Hello world from wxWidgets!"); }, ID_Hello);
        Bind(wxEVT_MENU, [](wxCommandEvent &event)
             { wxMessageBox("This is a wxWidgets Hello World example",
                            "About Hello World", wxOK | wxICON_INFORMATION); }, wxID_ABOUT);
        Bind(wxEVT_MENU, [this](wxCommandEvent &event)
             { this->Close(true); }, wxID_EXIT);
    }
};

class MyApp : public wxApp
{
public:
    bool OnInit() override
    {
        MyFrame *frame = new MyFrame();
        frame->Show(true);
        return true;
    }
};

wxIMPLEMENT_APP(MyApp);

注意Bind()函数传入的参数个数,这里不再需要传入this指针。使用Lambda表达式改写之后窗口类的成员函数基本不需要定义,需要调用对象自身的方法执行特定操作时(比如执行关闭窗口操作),可以通过捕获列表获取对象的this指针。

1.2 CMake配置文件

这里还是用cmake来管理项目构建。编写CMakeLists.txt文件如下。

# --- CMakeLists.txt ---

cmake_minimum_required(VERSION 3.21)
set(PROJECT_NAME wx_sample)
project(${PROJECT_NAME} LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)

if(NOT "$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
  set(VCPKG_TARGET_TRIPLET $ENV{VCPKG_TARGET_TRIPLET})
  message(STATUS "VCPKG_TARGET_TRIPLET: ${VCPKG_TARGET_TRIPLET}")
endif()

if(MINGW)
  message(STATUS "Set mingw cxx flags: -mwindows")
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mwindows")
endif()

# find wxWidgets
find_package(wxWidgets REQUIRED COMPONENTS core base)

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

add_executable(${PROJECT_NAME} main.cpp)

if(MSVC)
  set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")
  message(STATUS "Set msvc link flags: /SUBSYSTEM:WINDOWS")

  if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
    set_property(TARGET ${PROJECT_NAME} PROPERTY
      MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
    message(STATUS "Set msvc runtime library: MultiThreaded$<$<CONFIG:Debug>:Debug>")
  endif()
endif()

target_link_libraries(${PROJECT_NAME} ${wxWidgets_LIBRARIES})

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

if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
  message(STATUS "Static build, no need to install runtime dependencies")
else()
  message(STATUS "Dynamic build, install runtime dependencies")
  install(CODE [[
    if("$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
      find_program(WXRC wxrc HINTS $ENV{PATH})
      if(NOT WXRC)
        message(FATAL_ERROR "wxrc not found in PATH")
      else()
        message(STATUS "wxrc found: ${WXRC}")
        cmake_path(GET WXRC PARENT_PATH WX_BIN_DIR)
        message(STATUS "wx bin dir: ${WX_BIN_DIR}")
      endif()
    else()
      set(WX_BIN_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/bin")
      if(NOT EXISTS ${WX_BIN_DIR})
        message(FATAL_ERROR "wx bin dir not found: ${WX_BIN_DIR}")
      endif()
    endif()
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
        UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
        EXECUTABLES $<TARGET_FILE:wx_sample>
        DIRECTORIES ${WX_BIN_DIR}
        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()
    ]])
endif()

这里改进了配置文件,如果使用msys2工具链或conda包管理工具,可以通过查找环境变量中的wxrc工具来识别wxWidgets的安装目录并复制相关依赖库文件。

1.3 CMake编译和发布

通过以下命令实现配置、编译和发布安装。

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

如果在Windows下通过vcpkg编译安装,需要预先设置环境变量,而且cmake配置时还需要传递vcpkg工具链路径。如果VCPKG_TARGET_TRIPLET指定的是静态版,比如x64-windows-static,发布时自动跳过复制依赖项步骤。

$Env:VCPKG_ROOT="D:/vcpkg"
$Env:VCPKG_TARGET_TRIPLET="x64-windows"
cmake -B build . `
 -DCMAKE_TOOLCHAIN_FILE="${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" 
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/install

1.4 VSCode配置文件

使用vscode作为开发工具,需要在.vscode目录下创建settings.json文件,定义cmake拓展工具的参数开关和环境变量。

// --- settings.json ---

{
    "cmake.environment": {
        "VCPKG_ROOT": "D:/vcpkg",
        "VCPKG_TARGET_TRIPLET": "x64-windows",
    },
    "cmake.configureArgs": [
        "-DCMAKE_TOOLCHAIN_FILE=${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
    ],
    "cmake.configureSettings": {
        "CMAKE_EXPORT_COMPILE_COMMANDS": true
    },
    "cmake.buildDirectory": "${workspaceFolder}/build",
    "cmake.installPrefix": "${workspaceFolder}/install",
    "cmake.buildBeforeRun": false,
    "cmake.automaticReconfigure": false,
    "cmake.configureOnOpen": false,
    "cmake.configureOnEdit": false,
}

这里定义构建目录为build,安装发布目录为install,禁用了自动刷新配置文件的操作,需要手动生成/刷新配置文件。

如果条件允许的话,推荐尽量使用静态编译,这样生成的安装包体积较小,发布也很方便。

7850c607717319e2138523fb365d03b0.png

2. FLTK回调函数

2.1 回调函数访问类成员

相比于其他UI库,FLTK事件处理很简单,就是函数指针和回调函数5。FLTK的回调函数只支持外部函数和静态方法,以往在窗口类中使用回调函数访问类的成员,需要通过静态成员函数和强制类型转换传递类实例的this指针6,写起来十分麻烦。下面是一个示例,在弹出对话框中修改主窗口标签显示的字符串。

// --- main.cpp ---

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Double_Window.H>
#include <FL/fl_ask.H>

class MyWindow : public Fl_Window
{
private:
    Fl_Button *btn;
    Fl_Box *labelBox;
    Fl_Input *dialogInput;
    static void dialogCallback(Fl_Widget *w, void *data)
    {
        MyWindow *window = (MyWindow *)data;
        if (window->dialogInput && window->dialogInput->value())
        {
            window->labelBox->copy_label(window->dialogInput->value());
        }
        if (w->window())
        {
            w->window()->hide();
            delete w->window();
        }
    }
    static void btnCallback(Fl_Widget *w, void *data)
    {
        MyWindow *window = (MyWindow *)data;
        Fl_Double_Window *dialog = new Fl_Double_Window(300, 150, "Modify Label");
        dialog->begin();
        Fl_Input *input = new Fl_Input(50, 30, 200, 30, "Label:");
        input->value(window->labelBox->label());
        window->dialogInput = input;
        Fl_Button *confirmBtn = new Fl_Button(100, 80, 100, 30, "OK");
        confirmBtn->callback(dialogCallback, window);
        dialog->end();
        dialog->set_modal();
        dialog->show();
    }
public:
    MyWindow(int w, int h, const char *title) : Fl_Window(w, h, title), dialogInput(nullptr)
    {
        begin();
        labelBox = new Fl_Box(50, 40, 200, 30, "Hello World");
        labelBox->box(FL_BORDER_BOX);
        labelBox->align(FL_ALIGN_CENTER | FL_ALIGN_INSIDE);
        btn = new Fl_Button(100, 80, 100, 30, "Modify Label");
        btn->callback(btnCallback, this);
        end();
    }
    ~MyWindow() { dialogInput = nullptr; }
};
int main()
{
    MyWindow win(300, 200, "FLTK Label Modifier");
    win.show();
    return Fl::run();
}

2.2 不带捕获列表的Lambda表达式

这里按钮的回调函数,使用Lambda表达式的话,写法如下。

...
        btn->callback([](Fl_Widget *w, void *data)
                      { 
            MyWindow *window = (MyWindow *)data;
            Fl_Double_Window *dialog = new Fl_Double_Window(300, 150, "Modify Label");
            dialog->begin();
            Fl_Input *input = new Fl_Input(50, 30, 200, 30, "Label:");
            input->value(window->labelBox->label());
            window->dialogInput = input;
            Fl_Button *confirmBtn = new Fl_Button(100, 80, 100, 30, "OK");
            confirmBtn->callback([](Fl_Widget *w, void *data){
                MyWindow *window = (MyWindow *)data;
                if (window->dialogInput && window->dialogInput->value())
                {
                    window->labelBox->copy_label(window->dialogInput->value());
                }
                if (w->window())
                {
                    w->window()->hide();
                    delete w->window();
                }
            }, window);
            dialog->end();
            dialog->set_modal();
            dialog->show(); }, this);
...

这种写法和前一节的静态成员函数写法看起来差不多,可以正常通过编译。但这里只是简单地把两个静态成员函数拆开并嵌套到按钮的回调函数里,没有使用到捕获列表。注意这里只有捕获列表为空才能通过编译。

callback()直接调用Lambda表达式的唯一限制是不能通过捕获列表获取外部变量,只能通过参数传递this指针。这是因为在编译器层面,存在捕获列表的Lambda表达式会被当做函数对象(functor)处理7,反之则按照匿名函数处理。FLTK的callback()函数默认将Lambda表达式隐式转换成外部匿名函数处理了,而且callback()不支持函数对象。因此当作为callback()参数的Lambda表达式包含捕获列表时,编译器会报错。

2.3 带捕获列表的Lambda表达式

硬是想要在callback()中使用带捕获列表的Lambda表达式,办法也是有的,那就是再套一层转换,尝试把lambda转换而来的函数对象再转换成函数指针8。可以参考以下写法,定义转换函数。

...
// Define a conversion function, 
// which can be included in a separate header file.
using CALLBACK0 = void (*)(Fl_Widget *);
template <typename F>
CALLBACK0 lambda2fun(F lambda)
{
    static auto l = lambda;
    return [](Fl_Widget *w)
    {
        l(w);
    };
}
...

callback()内部再套一层上面的转换函数,将参数调用的带捕获列表的Lambda转换成不带捕获列表的Lambda表达式,避免编译器报错。

...
        // Wrap another conversion function inside the callback() function
        btn->callback(lambda2fun([=](Fl_Widget *w)
                                 { 
            Fl_Double_Window *dialog = new Fl_Double_Window(300, 150, "Modify Label");
            dialog->begin();
            Fl_Input *input = new Fl_Input(50, 30, 200, 30, "Label:");
            input->value(labelBox->label());
            dialogInput = input;
            Fl_Button *confirmBtn = new Fl_Button(100, 80, 100, 30, "OK");
            confirmBtn->callback(lambda2fun([=](Fl_Widget *w){
                if (dialogInput && dialogInput->value())
                {
                    labelBox->copy_label(dialogInput->value());
                }
                if (w->window())
                {
                    w->window()->hide();
                    delete w->window();
                }
            }));
            dialog->end();
            dialog->set_modal();
            dialog->show(); }));
...

这里的捕获列表[=]默认按传值方式捕获所有变量,包括this指针。在Lambda函数体里对成员变量的访问隐式传递了this指针调用,不需要再显示指定this->操作,提高了可读性。

2.4 构建、编译和发布

cmake配置文件CMakeLists.txt编写如下。fltk发布的程序体积非常小,一般只需要静态编译即可。考虑到某些平台比如msys2无法实现静态编译而且总是要带上编译器的运行时依赖库,还是把发布时自动复制依赖的内容加上比较稳妥。

# --- CMakeLists.txt ---

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

if(NOT "$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
  set(VCPKG_TARGET_TRIPLET $ENV{VCPKG_TARGET_TRIPLET})
  message(STATUS "VCPKG_TARGET_TRIPLET: ${VCPKG_TARGET_TRIPLET}")
endif()

# find fltk
find_package(FLTK REQUIRED)

if(FLTK_FOUND)
  message(STATUS "Found FLTK: ${FLTK_VERSION}")
  message(STATUS "FLTK_INCLUDE_DIR: ${FLTK_INCLUDE_DIR}")
  include_directories(${FLTK_INCLUDE_DIR})
  message(STATUS "FLTK_LIBRARIES: ${FLTK_LIBRARIES}")
else()
  message(FATAL_ERROR "FLTK not found")
endif()

add_executable(${PROJECT_NAME} main.cpp)

if(MSVC)
  target_link_options(${PROJECT_NAME} PRIVATE "/SUBSYSTEM:WINDOWS" "/ENTRY:mainCRTStartup")
  message(STATUS "Set msvc link flags: /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")

  if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
    set_property(TARGET ${PROJECT_NAME} PROPERTY
      MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
    message(STATUS "Set msvc runtime library: MultiThreaded$<$<CONFIG:Debug>:Debug>")
  endif()
elseif(MINGW)
  target_link_options(${PROJECT_NAME} PRIVATE "-mwindows")
  message(STATUS "Set mingw link flags: -mwindows")
endif()

target_link_libraries(${PROJECT_NAME} ${FLTK_LIBRARIES})

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

if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
  message(STATUS "Static build, no need to install runtime dependencies")
else()
  message(STATUS "Dynamic build, install runtime dependencies")
  install(CODE [[
    if("$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
      find_program(FLUID fluid HINTS $ENV{PATH})
      if(NOT FLUID)
        message(FATAL_ERROR "fluid not found in PATH")
      else()
        message(STATUS "fluid found: ${FLUID}")
        cmake_path(GET FLUID PARENT_PATH FLTK_BIN_DIR)
        message(STATUS "fltk bin dir: ${FLTK_BIN_DIR}")
      endif()
    else()
      set(FLTK_BIN_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/bin")
      if(NOT EXISTS ${FLTK_BIN_DIR})
        message(FATAL_ERROR "fltk bin dir not found: ${FLTK_BIN_DIR}")
      endif()
    endif()
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
        UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
        # path to the executable files
        EXECUTABLES $<TARGET_FILE:fltk_sample>
        # directories to search for library files
        DIRECTORIES ${FLTK_BIN_DIR}
        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()
    ]])
endif()

在终端上cmake的编译和发布命令同1.3 。使用VSCode的话,配置文件和1.4 一样,不重复了。

程序运行效果如下。

dc573ce4d6b89bc9bcce5ebdd6e2a1b9.png

3. Qt信号和槽

3.1 信号槽机制

不同于事件处理和回调函数,qt通过信号槽机制9实现对象间通信。但信号槽机制超脱于C++标准,更多依赖于qt自身的实现,需要通过qt自身工具对qt源码进行元编程。所幸qt槽函数本身支持Lambda表达式,不需要像FLTK回调函数一样进行包装转换。

下面实现了2.1 程序相同的功能,通过按钮弹出对话框修改主窗口的成员变量。

// --- main.cpp ---

#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QDialog>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <QMessageBox>

class InputDialog : public QDialog
{
    Q_OBJECT

public:
    InputDialog(const QString &initialText, QWidget *parent = nullptr)
        : QDialog(parent)
    {
        setWindowTitle("Input");
        setModal(true);
        setFixedSize(300, 120);
        QVBoxLayout *layout = new QVBoxLayout(this);
        QLabel *label = new QLabel("Input string:", this);
        lineEdit = new QLineEdit(initialText, this);
        QDialogButtonBox *buttonBox = new QDialogButtonBox(
            QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
        layout->addWidget(label);
        layout->addWidget(lineEdit);
        layout->addWidget(buttonBox);
        connect(buttonBox, &QDialogButtonBox::accepted, this, &InputDialog::accept);
        connect(buttonBox, &QDialogButtonBox::rejected, this, &InputDialog::reject);
    }
    QString getText() const { return lineEdit->text(); }

private:
    QLineEdit *lineEdit;
};
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        setWindowTitle("Qt6 String Editor");
        setFixedSize(400, 200);
        QWidget *centralWidget = new QWidget(this);
        setCentralWidget(centralWidget);
        QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
        label = new QLabel("Hello World", this);
        label->setAlignment(Qt::AlignCenter);
        label->setStyleSheet("font-size: 18px; font-weight: bold; padding: 20px;");
        QPushButton *button = new QPushButton("Edit String", this);
        button->setFixedSize(120, 40);
        mainLayout->addWidget(label);
        mainLayout->addWidget(button, 0, Qt::AlignCenter);
        connect(button, &QPushButton::clicked, this, [this]()
                {   InputDialog dialog(label->text(), this);
                    if (dialog.exec() == QDialog::Accepted)
                    {
                        QString newText = dialog.getText();
                        if (!newText.isEmpty())
                            label->setText(newText);
                        else
                            QMessageBox::warning(this, "Warning", "Input string cannot be empty!");
        } });
    }

private:
    QLabel *label;
};
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow window;
    window.show();
    return app.exec();
}
#include "main.moc"

主窗口按钮事件连接到带捕获的Lambda表达式,可以正常通过编译。

3.2 构建配置

cmake配置文件CMakeLists.txt编写如下。

# --- CMakeLists.txt ---

cmake_minimum_required(VERSION 3.21)
set(PROJECT_NAME qt_sample)
project(${PROJECT_NAME} VERSION 0.0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT "$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
  set(VCPKG_TARGET_TRIPLET $ENV{VCPKG_TARGET_TRIPLET})
  message(STATUS "VCPKG_TARGET_TRIPLET: ${VCPKG_TARGET_TRIPLET}")
endif()

# find qt6
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)

if(Qt6_FOUND)
  message(STATUS "Qt6 found: ${Qt6_DIR}")
else()
  message(FATAL_ERROR "Qt6 not found!")
endif()

set(PROJECT_SOURCES
  main.cpp
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
  qt_add_executable(${PROJECT_NAME}
    MANUAL_FINALIZATION
    ${PROJECT_SOURCES}
  )
else()
  if(ANDROID)
    add_library(${PROJECT_NAME} SHARED
      ${PROJECT_SOURCES}
    )
  else()
    add_executable(${PROJECT_NAME}
      ${PROJECT_SOURCES}
    )
  endif()
endif()

target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

if(MSVC AND(${VCPKG_TARGET_TRIPLET} MATCHES "static"))
  set_property(TARGET ${PROJECT_NAME} PROPERTY
    MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
  message(STATUS "Set msvc runtime library: MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

if(${QT_VERSION} VERSION_LESS 6.1.0)
  set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.test)
endif()

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

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

if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
  message(STATUS "Static build, no need to install runtime dependencies")
else()
  message(STATUS "Dynamic build, install runtime dependencies")
  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_app_script(
    TARGET ${PROJECT_NAME}
    OUTPUT_SCRIPT deploy_script
    NO_UNSUPPORTED_PLATFORM_ERROR
    DEPLOY_TOOL_OPTIONS ${deploy_tool_options_arg}
  )
  install(SCRIPT ${deploy_script})
  install(CODE [[
    if("$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
      find_program(QMAKE qmake6 HINTS $ENV{PATH})
      if(NOT QMAKE)
        message(FATAL_ERROR "qmake not found in PATH")
      else()
        message(STATUS "fluid found: ${QMAKE}")
        cmake_path(GET QMAKE PARENT_PATH QT_BIN_DIR)
        message(STATUS "qmake bin dir: ${QT_BIN_DIR}")
      endif()
    else()
      set(QT_BIN_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/bin")
      if(NOT EXISTS ${QT_BIN_DIR})
        message(FATAL_ERROR "qmake bin dir not found: ${QT_BIN_DIR}")
      endif()
    endif()
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
        UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
        # path to the executable files
        EXECUTABLES $<TARGET_FILE:qt_sample>
        # directories to search for library files
        DIRECTORIES ${QT_BIN_DIR}
        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()
    ]])
endif()

3.3 编译发布

cmake的编译和发布命令同1.3 。VSCode的配置文件同1.4 一样。程序运行界面如下。

c844301af217284e8d547f171dfd9496.png

4. gtk信号机制

4.1 gtk信号和回调函数

gtk通过事件10和信号11机制,将信号和函数指针绑定,实现事件处理。由于gtk以纯C为主要开发语言,而纯C标准并不支持面向对象特性,需要扩展至C++11及以上标准才能实现对Lambda表达式的支持。

使用纯C的信号链接函数指针的方式实现2.1 程序相同的功能,同样的通过按钮弹出对话框修改主窗口的成员变量,代码如下所示。

// --- c17/pure_c.c ---

#include <gtk/gtk.h>

static GtkWidget *main_label = NULL;

static void on_dialog_confirm(GtkWidget *widget, gpointer user_data)
{
    GtkWidget *dialog = GTK_WIDGET(user_data);
    GtkWidget *entry = GTK_WIDGET(g_object_get_data(G_OBJECT(dialog), "entry"));
    const gchar *new_text = gtk_editable_get_text(GTK_EDITABLE(entry));
    gtk_label_set_text(GTK_LABEL(main_label), new_text);
    gtk_window_destroy(GTK_WINDOW(dialog));
}

static void on_dialog_cancel(GtkWidget *widget, gpointer user_data)
{
    GtkWidget *dialog = GTK_WIDGET(user_data);
    gtk_window_destroy(GTK_WINDOW(dialog));
}

static void on_main_button_clicked(GtkWidget *widget, gpointer user_data)
{
    GtkWidget *main_window = GTK_WIDGET(user_data);
    GtkWidget *dialog = gtk_window_new();
    gtk_window_set_title(GTK_WINDOW(dialog), "Edit text");
    gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(main_window));
    gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
    gtk_window_set_default_size(GTK_WINDOW(dialog), 300, 150);
    GtkWidget *dialog_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_window_set_child(GTK_WINDOW(dialog), dialog_box);
    gtk_widget_set_margin_top(dialog_box, 10);
    gtk_widget_set_margin_bottom(dialog_box, 10);
    gtk_widget_set_margin_start(dialog_box, 10);
    gtk_widget_set_margin_end(dialog_box, 10);
    GtkWidget *entry = gtk_entry_new();
    GtkEntryBuffer *buffer = gtk_entry_get_buffer(GTK_ENTRY(entry));
    const gchar *old_text = gtk_label_get_text(GTK_LABEL(main_label));
    gtk_entry_buffer_set_text(buffer, old_text ? old_text : "Hello World", -1);
    gtk_box_append(GTK_BOX(dialog_box), entry);
    GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
    gtk_box_append(GTK_BOX(dialog_box), button_box);
    GtkWidget *confirm_button = gtk_button_new_with_label("Confirm");
    GtkWidget *cancel_button = gtk_button_new_with_label("Cancel");
    gtk_box_append(GTK_BOX(button_box), confirm_button);
    gtk_box_append(GTK_BOX(button_box), cancel_button);
    g_object_set_data(G_OBJECT(dialog), "entry", entry);
    g_signal_connect(confirm_button, "clicked", G_CALLBACK(on_dialog_confirm), dialog);
    g_signal_connect(cancel_button, "clicked", G_CALLBACK(on_dialog_cancel), dialog);
    gtk_window_present(GTK_WINDOW(dialog));
}

static void on_activate(GtkApplication *app, gpointer user_data)
{
    GtkWidget *main_window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(main_window), "GTK4 Sample");
    gtk_window_set_default_size(GTK_WINDOW(main_window), 400, 200);
    GtkWidget *main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_window_set_child(GTK_WINDOW(main_window), main_box);
    gtk_widget_set_margin_top(main_box, 20);
    gtk_widget_set_margin_bottom(main_box, 20);
    gtk_widget_set_margin_start(main_box, 20);
    gtk_widget_set_margin_end(main_box, 20);
    main_label = gtk_label_new("Hello World");
    GtkWidget *button = gtk_button_new_with_label("Edit text");
    gtk_box_append(GTK_BOX(main_box), main_label);
    gtk_box_append(GTK_BOX(main_box), button);
    g_signal_connect(button, "clicked", G_CALLBACK(on_main_button_clicked), main_window);
    gtk_window_present(GTK_WINDOW(main_window));
}

int main(int argc, char *argv[])
{
    GtkApplication *app = gtk_application_new("com.example.gtk_sample", G_APPLICATION_DEFAULT_FLAGS);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);
    int status = g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);
    return status;
}

4.2 Lambda表达式实现

参考2.3 ,函数指针用带捕获的Lambda表达式替代。注意这里用到了C++的语法,应在编译器开关或者构建系统中打开对C++支持。

// --- cpp17/cpp_lambda.cpp ---

#include <gtk/gtk.h>

// Conversion function,
using CALLBACK1 = void (*)(GtkWidget *, gpointer);
template <typename F>
CALLBACK1 lambda2fun(F lambda)
{
    static auto l = lambda;
    return [](GtkWidget *w, gpointer user_data)
    {
        l(w, user_data);
    };
}

static void on_activate(GtkApplication *app, gpointer user_data)
{
    // main window
    GtkWidget *main_window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(main_window), "GTK4 Sample");
    gtk_window_set_default_size(GTK_WINDOW(main_window), 400, 200);
    GtkWidget *main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_window_set_child(GTK_WINDOW(main_window), main_box);
    gtk_widget_set_margin_top(main_box, 20);
    gtk_widget_set_margin_bottom(main_box, 20);
    gtk_widget_set_margin_start(main_box, 20);
    gtk_widget_set_margin_end(main_box, 20);
    GtkWidget *main_label = gtk_label_new("Hello World");
    GtkWidget *button = gtk_button_new_with_label("Edit text");
    gtk_box_append(GTK_BOX(main_box), main_label);
    gtk_box_append(GTK_BOX(main_box), button);
    // entry dialog
    GtkWidget *dialog = gtk_window_new();
    gtk_window_set_title(GTK_WINDOW(dialog), "Edit text");
    gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(main_window));
    gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
    gtk_window_set_hide_on_close(GTK_WINDOW(dialog), TRUE);
    gtk_window_set_default_size(GTK_WINDOW(dialog), 300, 150);
    GtkWidget *dialog_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_window_set_child(GTK_WINDOW(dialog), dialog_box);
    gtk_widget_set_margin_top(dialog_box, 10);
    gtk_widget_set_margin_bottom(dialog_box, 10);
    gtk_widget_set_margin_start(dialog_box, 10);
    gtk_widget_set_margin_end(dialog_box, 10);
    GtkWidget *entry = gtk_entry_new();
    GtkEntryBuffer *buffer = gtk_entry_get_buffer(GTK_ENTRY(entry));
    gtk_box_append(GTK_BOX(dialog_box), entry);
    GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
    gtk_box_append(GTK_BOX(dialog_box), button_box);
    GtkWidget *confirm_button = gtk_button_new_with_label("Confirm");
    GtkWidget *cancel_button = gtk_button_new_with_label("Cancel");
    gtk_box_append(GTK_BOX(button_box), confirm_button);
    gtk_box_append(GTK_BOX(button_box), cancel_button);
    // connect signals
    g_signal_connect(confirm_button, "clicked", G_CALLBACK(lambda2fun([dialog, main_label, buffer](GtkWidget *widget, gpointer user_data)
                                                                      {
        const gchar *new_text = gtk_entry_buffer_get_text(buffer);
        gtk_label_set_text(GTK_LABEL(main_label), new_text);
        gtk_window_close(GTK_WINDOW(dialog)); })),
                     NULL);
    g_signal_connect(cancel_button, "clicked", G_CALLBACK(lambda2fun([dialog](GtkWidget *widget, gpointer user_data)
                                                                     { gtk_window_close(GTK_WINDOW(dialog)); })),
                     NULL);

    g_signal_connect(button, "clicked", G_CALLBACK(lambda2fun([dialog, buffer, main_label](GtkWidget *widget, gpointer user_data)
                                                              { 
        const gchar *old_text = gtk_label_get_text(GTK_LABEL(main_label));
        gtk_entry_buffer_set_text(buffer, old_text ? old_text : "Hello World", -1);
        gtk_window_present(GTK_WINDOW(dialog)); })),
                     NULL);
    gtk_window_present(GTK_WINDOW(main_window));
}

int main(int argc, char *argv[])
{
    GtkApplication *app = gtk_application_new("com.example.gtk_sample", G_APPLICATION_DEFAULT_FLAGS);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);
    int status = g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);
    return status;
}

通过Lambda表达式的捕获列表,可以很方便地访问外部环境变量,避免了定义全局变量导致的可读性和安全性问题。

4.3 捕获变量的生命周期问题

使用捕获列表尤其要注意捕获变量的生命周期问题。细心的朋友可能已经发现了,完全照搬纯C代码实现的话,上面的例子中Lambda表达式可以写成下面的形式。

...
    g_signal_connect(button, "clicked", G_CALLBACK(lambda2fun([main_window, main_label](GtkWidget *widget, gpointer user_data)
                                                              {
        GtkWidget *dialog = gtk_window_new();
        gtk_window_set_title(GTK_WINDOW(dialog), "Edit text");
        gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(main_window));
        gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
        gtk_window_set_default_size(GTK_WINDOW(dialog), 300, 150);
        GtkWidget *dialog_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
        gtk_window_set_child(GTK_WINDOW(dialog), dialog_box);
        gtk_widget_set_margin_top(dialog_box, 10);
        gtk_widget_set_margin_bottom(dialog_box, 10);
        gtk_widget_set_margin_start(dialog_box, 10);
        gtk_widget_set_margin_end(dialog_box, 10);
        GtkWidget *entry = gtk_entry_new();
        GtkEntryBuffer *buffer = gtk_entry_get_buffer(GTK_ENTRY(entry));
        const gchar *old_text = gtk_label_get_text(GTK_LABEL(main_label));
        gtk_entry_buffer_set_text(buffer, old_text ? old_text : "Hello World", -1);
        gtk_box_append(GTK_BOX(dialog_box), entry);
        GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
        gtk_box_append(GTK_BOX(dialog_box), button_box);
        GtkWidget *confirm_button = gtk_button_new_with_label("Confirm");
        GtkWidget *cancel_button = gtk_button_new_with_label("Cancel");
        gtk_box_append(GTK_BOX(button_box), confirm_button);
        gtk_box_append(GTK_BOX(button_box), cancel_button);
        g_signal_connect(confirm_button, "clicked", G_CALLBACK(lambda2fun([main_label, dialog, buffer](GtkWidget *widget, gpointer user_data){
            const gchar *new_text = gtk_entry_buffer_get_text(buffer);
            gtk_label_set_text(GTK_LABEL(main_label), new_text);
            gtk_window_destroy(GTK_WINDOW(dialog));
        })), NULL);
        g_signal_connect(cancel_button, "clicked", G_CALLBACK(lambda2fun([dialog](GtkWidget *widget, gpointer user_data){
            gtk_window_destroy(GTK_WINDOW(dialog));
        })), NULL);
        gtk_window_present(GTK_WINDOW(dialog)); })),
                     NULL);
    gtk_window_present(GTK_WINDOW(main_window));
...

上面的代码虽然能正常通过编译,但是多次点击弹出的对话框按钮很有可能出现错误。

这种写法直接将对话框的初始化和信号连接操作内嵌到主窗口按钮所调用的Lambda表达式中,但是对话框所调用的两个Lambda表达式中所捕获的对话框窗体和资源是局部变量。对话框局部变量的生命周期将随着对话框资源的释放而销毁,但Lambda表达式的捕获列表却不会更新,导致下一次调用时出现错误。

比较好的解决办法,就像上一节展示的那样。要保证捕获的变量在Lambda表达式的执行周期内不会被释放,避免在Lambda表达式中初始化和销毁捕获的资源, 也要避免带捕获的Lambda表达式互相嵌套。

4.4 构建配置

cmake配置文件CMakeLists.txt编写如下。虽然gtk是基于C编写的,但这里用到了C++的特性,应打开C++支持。

# --- cpp17/CMakeLists.txt ---

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

if(NOT "$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
  set(VCPKG_TARGET_TRIPLET $ENV{VCPKG_TARGET_TRIPLET})
  message(STATUS "VCPKG_TARGET_TRIPLET: ${VCPKG_TARGET_TRIPLET}")
endif()

set(SRC_LIST cpp_lambda.cpp)

if(MINGW)
  set(CMAKE_C_FLAGS "-mwindows")
  set(CMAKE_CXX_FLAGS "-mwindows")
endif()

find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED gtk4)
include_directories(${GTK_INCLUDE_DIRS})
link_directories(${GTK_LIBRARY_DIRS})
add_executable(${PROJECT_NAME} ${SRC_LIST})

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

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}
)

if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
  message(STATUS "Static build, no need to install runtime dependencies")
else()
  message(STATUS "Dynamic build, install runtime dependencies")
  install(CODE [[
    if("$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
      find_program(GTK4DEMO gtk4-demo HINTS $ENV{PATH})
      if(NOT GTK4DEMO)
        message(FATAL_ERROR "gtk4-demo not found in PATH")
      else()
        message(STATUS "gtk4-demo found: ${GTK4DEMO}")
        cmake_path(GET GTK4DEMO PARENT_PATH GTK4_BIN_DIR)
        message(STATUS "gtk4-demo bin dir: ${GTK4_BIN_DIR}")
      endif()
    else()
      set(GTK4_BIN_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/bin")
      if(NOT EXISTS ${GTK4_BIN_DIR})
        message(FATAL_ERROR "gtk4-demo bin dir not found: ${GTK4_BIN_DIR}")
      endif()
    endif()
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
        UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
        # path to the executable files
        EXECUTABLES $<TARGET_FILE:gtk_sample_lambda>
        # directories to search for library files
        DIRECTORIES ${GTK4_BIN_DIR}
        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_path(GET GTK4_BIN_DIR PARENT_PATH GTK_SEARCH_DIRS)
    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()
    ]])
endif()

4.5 编译发布

cmake的编译发布命令以及VSCode的配置文件同1.31.4 ,程序运行效果如下。

99a2cadc419a4a80695d1d84e56c4e5b.png

5. 总结

对于wxWidgets和qt这些原生支持Lambda表达式的UI库,推荐尽量多去用Lambda表达式。实际开发中很多UI事件的处理通常也就几行代码,犯不着为了区区数行代码去单开一个函数或者一个文件,这时候使用Lambda表达式可以很好地起到简化代码的效果。

对FLTK和gtk这种,虽然不直接支持Lambda表达式,但还是有办法将其应用到项目中。使用Lambda表达式可以通过捕获列表很方便地实现参数传递,不再受限于回调函数的传参操作,同时也很好地避免了全局变量定义操作。只是需要注意捕获参数的生命周期问题。