Andrew Moa Blog Site

探索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

使用conda管理C++项目依赖

之前尝试xrepo的时候了解到conda也可以管理C++项目1,现在针对常用的库验证下。conda安装过程不细说,建议参考官方文档2。由于conda提供的库多以动态链接形式发布,为了确保程序打包输出能正常运行,尝试通过cmake自动打包输出依赖的库文件。

1. 编译CGAL+qt项目

1.1 建立虚拟环境

首先要建立并激活虚拟环境。conda自带的默认虚拟环境为base,命名上注意不要和默认虚拟环境重名。这里通过虚拟环境安装依赖,避免不同项目之间的依赖产生冲突。

conda create -n cgal_env # 建立虚拟环境,-n 指定虚拟环境名称
conda activate cgal_env # 激活虚拟环境

虚拟环境激活之后会在shell前面显示(cgal_env)。如果是安装conda之后第一次建立虚拟环境,激活的时候可能会报错,一般重启shell即可解决。

激活虚拟环境之后,在虚拟环境中安装cgal包及其依赖。这里碰到一个问题,conda的托管的cgal不会自动安装依赖的qt6,得手动安装。

conda install cgal -c conda-forge	# -c 指定通道 conda-forge
conda install qt6-main -c conda-forge

1.2 编写工程文件

以官方教程3为例,建立cmake项目文件夹surface_mesh_viewer。编写C++源文件main.cpp如下所示。

// main.cpp

/***
 * reference: https://doc.cgal.org/latest/Manual/devman_create_and_use_a_cmakelist.html
 ***/

#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/draw_surface_mesh.h>
#include <fstream>
#include <QApplication>
#include <QFileDialog>

typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point;
typedef CGAL::Surface_mesh<Point> Mesh;

int main(int argc, char *argv[])
{
  QApplication app(argc, argv);
  const std::string filename = (argc > 1) ? argv[1] : QFileDialog::getOpenFileName(nullptr, "Open a mesh file", "", "Supported formats (*.off *.stl *.obj *.ply);;OFF format (*.off);;STL format (*.stl);;OBJ format (*.obj);;PLY format (*.ply)").toStdString();
  if (filename.empty())
    return EXIT_FAILURE;

  Mesh sm;
  if (!CGAL::IO::read_polygon_mesh(filename, sm))
  {
    if (filename.substr(filename.find_last_of(".") + 1) == "stl")
      std::cerr << "Invalid STL file: " << filename << std::endl;
    else if (filename.substr(filename.find_last_of(".") + 1) == "obj")
      std::cerr << "Invalid OBJ file: " << filename << std::endl;
    else if (filename.substr(filename.find_last_of(".") + 1) == "ply")
      std::cerr << "Invalid PLY file: " << filename << std::endl;
    else if (filename.substr(filename.find_last_of(".") + 1) == "off")
      std::cerr << "Invalid OFF file: " << filename << std::endl;
    else
      std::cerr << "Invalid file: " << filename << "(Unknown file format.)" << std::endl;
    return EXIT_FAILURE;
  }

  // Internal color property maps are used if they exist and are called
  // "v:color", "e:color" and "f:color".
  auto vcm =
      sm.add_property_map<Mesh::Vertex_index, CGAL::IO::Color>("v:color").first;
  auto ecm =
      sm.add_property_map<Mesh::Edge_index, CGAL::IO::Color>("e:color").first;
  auto fcm = sm.add_property_map<Mesh::Face_index>(
                   "f:color", CGAL::IO::white() /*default*/)
                 .first;

  for (auto v : vertices(sm))
  {
    if (v.idx() % 2)
    {
      put(vcm, v, CGAL::IO::black());
    }
    else
    {
      put(vcm, v, CGAL::IO::blue());
    }
  }

  for (auto e : edges(sm))
  {
    put(ecm, e, CGAL::IO::gray());
  }

  put(fcm, *(sm.faces().begin()), CGAL::IO::red());

  // Draw!
  CGAL::draw(sm);

  return EXIT_SUCCESS;
}

这里用到了cmake作为编译工具,编写CMakeLists.txt文件。

阅读时长6分钟
Andrew Moa

使用xmake编译C/C++项目

原来习惯了在Windows下使用cmake+vcpkg编译C/C++程序,但是vcpkg有个缺点,每次更新都需要从github下载源码编译库文件。像qt那样的大型库,每次光是编译动不动就浪费一整天时间实在耗不起,更别说DNS导致的连接问题了。

先梳理下需求,对C/C++包管理器的要求主要以下3点:

  1. 支持gtk+、qt、wxWidgets、cgal、vtk等常用库。
  2. 支持跨平台和IDE集成,或者支持CMake。
  3. 及时更新。

能满足条件的除了vcpkg就只剩xrepo了。 xrepo是xmake自带的包管理器,xmake1是一个新兴的C/C++构建工具。这篇文章尝试使用xmake+repo替代cmake+vcpkg,看能否满足现阶段C/C++程序的编译需求。

1. 安装xmake

按照官方文档2的指导,这里用posershell命令直接下载安装:

irm https://xmake.io/psget.text | iex

在Windows下执行以上命令,自动安装在C:\Users\[用户名]\xmake路径下。

也可以使用其他方式安装,比如直接从Release 页面下载对应平台的安装包手动安装。

安装完xmake之后xrepo就可以使用了,查看xmake、xrepo的版本:

xmake --version
xrepo --version

cd8eda41c5999fbe3a3714490ad3369c.png

vscode可以安装以下拓展,用于查看和构建xmake项目。

2. 创建项目

可以直接使用xmake的create命令创建项目,默认为C++项目。

xmake create hello_cpp

f122d0c81fc444640af563890205868d.png

也可以增加-l开关指定项目的语言类型。

xmake create -l c hello_c

打开hello_c项目自动生成的xmake.lua文件,后面一大段注释告诉用户怎么配置、构建和安装项目。直接在项目文件夹中执行xmake命令即可编译。

使用vscode安装了拓展的话,也可以通过下方的按钮执行编译命令。

4d7371ad8f240c02d2c5e07b6c104cd2.png

阅读时长4分钟
Andrew Moa