Andrew Moa Blog Site

CPack打包程序依赖问题

之前用CPack打包QML程序的时候发现,CPack打包程序安装的文件和使用cmake install命令安装的文件存在明显的不同,下面解释并解决这个问题。

1. 问题代码

还是以之前编写的QML程序为例,为了方便改进代码将CMakeLists.txt改写如下,依赖安装和打包模块分别写入cmake/dependencies.cmakecmake/packaging.cmake

cmake_minimum_required(VERSION 3.21)
set(PROJECT_NAME sample)
project(${PROJECT_NAME} VERSION 0.1 LANGUAGES CXX)
set(EXECUTABLE_NAME ${PROJECT_NAME}_app)
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}")
  link_libraries("$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/lib")
  message(STATUS "Link libraries from: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/$<$<CONFIG:Debug>:Debug>/lib")
  include_directories("$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/include")
  message(STATUS "Include directories from: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/include")
  if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
    set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake")
    message(STATUS "Using static Qt6 toolchain file: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake")
    set(Qt6_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6")
    message(STATUS "Using static Qt6_DIR: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6")
  endif()
endif()

find_package(Qt6 6.5 REQUIRED COMPONENTS Quick)

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

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

# set static linking for MSVC
if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Configuring for MSVC static linking")
    set_property(TARGET ${EXECUTABLE_NAME} PROPERTY
        MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

set_target_properties(${EXECUTABLE_NAME} PROPERTIES

    # MACOSX_BUNDLE_GUI_IDENTIFIER com.example.${EXECUTABLE_NAME}
    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(${EXECUTABLE_NAME}
    PRIVATE Qt6::Quick
)

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

if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Linking static MSVC runtime, skipping install of MSVC runtime DLLs")
else()
    message(STATUS "Configuring install of runtime dependencies")
    include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/dependencies.cmake)
endif()

include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/packaging.cmake)

cmake/dependencies.cmake模块定义了如何从qt安装路径中复制依赖带安装路径。

阅读时长4分钟
Andrew Moa

Lambda表达式处理事件回调函数

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指针。

阅读时长11分钟
Andrew Moa

探索UI和业务代码分离的实现方法

将程序界面和业务代码分离对提高开发效率和代码可读性是很有帮助的,主流的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传入。

阅读时长7分钟
Andrew Moa