# qwfassoc 实施计划 本文档是 qwfassoc GUI 程序的详细实施计划。qwfassoc 是 wfassoc 动态链接库的可视化界面,用于管理 Windows 应用程序的注册/卸载以及文件关联。 --- ## 1. 项目结构 ``` example/qwfassoc/ ├── TASKS.md # 本文件 ├── CMakeLists.txt # CMake 构建文件 ├── cmake/ │ ├── Findwfassoc.cmake # 从 wfassoc-cdylib/codegen/Findwfassoc.cmake 复制 │ └── README.md # 说明 Findwfassoc.cmake 的来源 └── src/ ├── main.cpp # 程序入口 + 命令行解析 ├── manifesto.hpp # TOML manifest 解析与校检模块的头文件 ├── manifesto.cpp # TOML manifest 解析与校检模块的实现文件 ├── mainwindow.ui # Qt Designer UI 文件(主对话框) ├── mainwindow.hpp # MainWindow 类头文件 └── mainwindow.cpp # MainWindow 类实现文件 ``` --- ## 2. 任务分解 ### 任务 2.1: 复制 cmake 模块文件 **操作内容:** 1. 将 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制到 `example/qwfassoc/cmake/Findwfassoc.cmake`。 2. 在 `example/qwfassoc/cmake/README.md` 中写入以下内容(纯文本说明文件): ```markdown # cmake 模块说明 此目录下的 `Findwfassoc.cmake` 是从项目根目录 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制而来。 该文件提供 `wfassoc::wfassoc` imported target。使用前需要设置 `wfassoc_ROOT` 变量指向 wfassoc 安装目录。 ``` --- ### 任务 2.2: 编写 CMakeLists.txt **产出文件:** `example/qwfassoc/CMakeLists.txt` **内容要求:** ```cmake cmake_minimum_required(VERSION 3.16) project(qwfassoc VERSION 0.1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Qt 自动处理 MOC/UIC set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) # --- 查找依赖库 --- # 查找 Qt6 Widgets find_package(Qt6 REQUIRED COMPONENTS Widgets) # 查找 toml11 find_package(toml11 REQUIRED) # 添加 cmake 模块搜索路径(用于 Findwfassoc.cmake) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # 查找 wfassoc (需要用户设置 wfassoc_ROOT) find_package(wfassoc REQUIRED) # --- 源文件 --- set(SOURCES src/main.cpp src/mainwindow.cpp src/manifesto.cpp ) set(HEADERS src/mainwindow.hpp src/manifesto.hpp ) set(UI_FILES src/mainwindow.ui ) # --- 构建目标 --- qt_add_executable(qwfassoc ${SOURCES} ${HEADERS} ${UI_FILES}) target_link_libraries(qwfassoc PRIVATE Qt6::Widgets toml11::toml11 wfassoc::wfassoc ) # 将 wfassoc DLL 复制到输出目录 add_custom_command(TARGET qwfassoc POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${wfassoc_DLL}" $ COMMENT "Copying wfassoc DLL to output directory" ) ``` **注意事项:** - 使用 `qt_add_executable` 而不是 `add_executable`,因为 Qt6 推荐用此命令自动设置 WIN32 等属性。 - 使用 `CMAKE_AUTOUIC ON` 自动处理 `.ui` 文件。 - 使用 `CMAKE_AUTOMOC ON` 自动处理 QObject 的 MOC。 - `wfassoc_DLL` 变量由 `Findwfassoc.cmake` 提供。 --- ### 任务 2.3: 编写 Manifest 解析与校检模块 本模块负责从 TOML 文件加载 manifest、进行校检,然后构建 `wfassocpp::Schema` 并创建 `wfassocpp::Program`。 **涉及 API:** wfassoc++.h 中的 `Schema`, `Program` 类。 **校检逻辑:** 参照 `wfassoc-exec/src/manifest.rs` 的实现: - `identifier`, `path`, `clsid` 是必填字段 - `name`, `icon`, `behavior` 是可选字段 - `[strs]`, `[icons]`, `[behaviors]`, `[exts]` 表格全部可选(可为空) - 使用 Schema 的各种 setter 方法写入值,任何底层验证失败都会通过 `_Check` 抛出 `std::runtime_error` --- #### 任务 2.3.1: 编写 manifesto.hpp **产出文件:** `example/qwfassoc/src/manifesto.hpp` **头文件内容规范:** ```cpp #pragma once #include "wfassoc++.h" #include #include // 从 TOML manifest 文件构建 Program // 内部完成 Schema 创建、字段填充、校检、Program 创建 // 如果任何步骤失败,抛出 std::runtime_error(由 wfassoc++ 的 _Check 机制产生) // 也可能抛出 toml11 的解析异常 std::unique_ptr BuildProgramFromManifest(const std::string& tomlPath); ``` 所有实现集中在 `manifesto.cpp` 中即可。此模块不对外暴露内部数据结构。 --- #### 任务 2.3.2: 编写 manifesto.cpp **产出文件:** `example/qwfassoc/src/manifesto.cpp` **实现流程:** 1. 使用 `toml::parse(tomlPath)` 解析 TOML 文件。如果解析失败,toml11 会抛出异常(通常是 `toml::syntax_error`)。直接让异常向上传播。 2. 从解析结果中提取数据: - 必填字段:`identifier`(字符串)、`path`(字符串)、`clsid`(字符串) - 可选字段:`name`(字符串,可为空)、`icon`(字符串,可为空)、`behavior`(字符串,可为空) - 表格:`strs`(键值对表)、`icons`(键值对表)、`behaviors`(键值对表)、`exts`(表,每个键对应一个子表) toml11 读取语法示例: ```cpp const auto& data = toml::parse(path); std::string identifier = toml::find(data, "identifier"); std::string path_val = toml::find(data, "path"); std::string clsid = toml::find(data, "clsid"); ``` 可选字段读取: ```cpp std::string name; bool has_name = data.contains("name"); if (has_name) { name = toml::find(data, "name"); } ``` (icon、behavior 同理) 表格读取: ```cpp const auto& strs = toml::find(data, "strs").as_table(); // 遍历 strs: for (const auto& [key, value] : strs) { ... value.as_string() ... } ``` exts 表格读取(每个值是嵌套表,包含 name、icon、behavior 字段): ```cpp const auto& exts = toml::find(data, "exts").as_table(); for (const auto& [ext_key, ext_value] : exts) { const auto& ext_table = ext_value.as_table(); // 这实际上是 toml::value 的嵌套访问,toml11 中 as_table() 返回的是 std::unordered_map> // 但更简单的是在解析后使用 toml::find(data, "exts") 然后再逐个读取 } ``` **toml11 访问注意事项:** 对于嵌套的 TOML 表(如 `[exts.jpg]`),toml11 中使用 `toml::find` 时路径可以用点号拼接,例如 `toml::find(data, "exts", "jpg", "name")` 或通过迭代 as_table 的结果来遍历。具体可以参考 toml11 的文档。 3. 创建 `wfassocpp::Schema` 对象并填充字段: ```cpp wfassocpp::Schema schema; // 必填字段 schema.SetIdentifier(identifier.c_str()); schema.SetPath(path_val.c_str()); schema.SetClsid(clsid.c_str()); // 可选字段:如果不存在则传 nullptr schema.SetName(has_name ? name.c_str() : nullptr); schema.SetIcon(has_icon ? icon.c_str() : nullptr); schema.SetBehavior(has_behavior ? behavior.c_str() : nullptr); // 遍历并添加 strs for (const auto& [key, value] : strs) { std::string v = toml::find(value, ""); // value 本身是字符串 // 实际上,遍历 as_table() 得到的 value 是 shared_ptr // 需要用 value->as_string() 或者解引用后获取 schema.AddStr(key.c_str(), /* value string */.c_str()); } // 同理遍历 icons 和 behaviors // 遍历 exts: ext_key 是纯扩展名(不含点),ext_table 有 name, icon, behavior 三个字符串字段 for (const auto& [ext_key, ext_table_value] : exts) { // 从 ext_table_value 中读取 name, icon, behavior schema.AddExt(ext_key.c_str(), ext_name.c_str(), ext_icon.c_str(), ext_behavior.c_str()); } ``` 4. 用 Schema 创建 Program: ```cpp return std::make_unique(std::make_unique(std::move(schema))); ``` **注意:** Program 的构造函数接受 `std::unique_ptr&&`。Schema 会被消费(move),之后不能再使用。 **错误处理:** - toml 解析错误 → 抛出 toml::syntax_error,上层捕获后弹出错误对话框 - Schema 设置错误 → wfassoc++ 抛出 std::runtime_error,上层捕获 - Program 创建错误 → wfassoc++ 抛出 std::runtime_error,上层捕获 --- ### 任务 2.4: 编写 UI 文件 (mainwindow.ui) **产出文件:** `example/qwfassoc/src/mainwindow.ui` **使用 Qt Designer 或手动编写 XML 格式的 .ui 文件。** #### 层级结构概览 ``` QDialog (窗口, objectName="MainWindow") ├── windowTitle: "" (运行时动态设置) ├── minimumSize: 480x600, maximumSize: 480x600 ├── Layout: QVBoxLayout │ └── QTabWidget (objectName="tabWidget") │ ├── Tab 0: "应用程序" │ │ └── QGroupBox (objectName="groupBoxApp", title="安装与卸载") │ │ └── Layout: QVBoxLayout │ │ ├── [上半部分] QHBoxLayout │ │ │ ├── QLabel (objectName="lblAppIcon", 显示应用程序图标) │ │ │ └── QLabel (objectName="lblAppDesc", wordWrap=true) │ │ └── [下半部分] QHBoxLayout │ │ ├── QPushButton (objectName="btnInstall", text="安装") │ │ └── QPushButton (objectName="btnUninstall", text="卸载") │ │ │ └── Tab 1: "文件关联" │ └── Layout: QVBoxLayout │ ├── QLabel (objectName="lblAssocDesc", wordWrap=true) │ ├── QHBoxLayout │ │ ├── QPushButton (objectName="btnSelectAllUser", text="+") │ │ └── QPushButton (objectName="btnSelectAllSystem", text="+") │ ├── QTableWidget (objectName="tableAssoc", columnCount=3) │ │ ├── 列 0 标题: "类型" │ │ ├── 列 1 标题: "" (运行时设为当前用户名) │ │ └── 列 2 标题: "所有用户" │ └── QHBoxLayout (底部按钮栏) │ ├── QPushButton (objectName="btnOk", text="确定", default=true) │ ├── QPushButton (objectName="btnCancel", text="取消") │ └── QPushButton (objectName="btnApply", text="应用") ``` #### 详细属性设置 **QDialog (MainWindow):** - `windowTitle`: 留空(运行时设置) - `minimumSize` / `maximumSize`: 宽度 480,高度 600 - `sizePolicy`: Fixed(水平和垂直都设为 Fixed) **QTabWidget (tabWidget):** - 添加两个标签页,标题分别为 `"应用程序"` 和 `"文件关联"`。 **QLabel (lblAppIcon):** - 用于显示从 `Program::ResolveIcon()` 获取的图标。需要设置 `scaledContents` 为 false,通过代码将 HICON 转换为 QPixmap 并设置。 - 建议 size: 48x48 左右(通过 `minimumSize` / `maximumSize` 固定)。 **QLabel (lblAppDesc):** - `wordWrap` 设为 true。 **QPushButton (btnInstall / btnUninstall):** - 初始都设为 enabled(代码中根据注册状态动态调整)。 **QLabel (lblAssocDesc):** - `wordWrap` 设为 true。 **QTableWidget (tableAssoc):** - `columnCount`: 3 - 列 0 标题:`"类型"` - 列 1 标题:`""`(运行时设为标准格式的用户名,如 "DESKTOP-XXX\username") - 列 2 标题:`"所有用户"` - `selectionBehavior`: `SelectItems` - `editTriggers`: `NoEditTriggers`(禁止编辑) - 水平表头设置:`stretchLastSection` 为 true(可选) - 添加垂直滚动条(默认行为即可) **底部按钮 (btnOk, btnCancel, btnApply):** - `btnOk`: `default` 属性设为 true(设为默认按钮) - `btnApply`: 初始 disabled(无修改时不可用) #### UI 文件中的布局提示 - **"应用程序"选项卡**中的 QGroupBox 应该拉伸占满标签页空间。 - QGroupBox 内部的布局使用 `QVBoxLayout`,上半部分(图标+文本描述)和下半部分(安装/卸载按钮)是其中的两个水平布局。 - 图标和文本之间可以用 `addStretch()` 或设置间距来保证合适的视觉效果。 - **"文件关联"选项卡**的最底部按钮栏(确定/取消/应用)应使用右对齐的布局。可以使用 `QHBoxLayout` 并在前面添加 `addStretch()` 来实现右对齐。 #### QTableWidget 列宽设置建议 在 .ui 文件中给列设置初始宽度: - 列 0 ("类型"):约 120 像素 - 列 1 (用户名):约 160 像素 - 列 2 ("所有用户"):约 160 像素 - (总共约 440 像素,接近对话框宽度 480 减去边距) 可以关闭水平表头的 `stretchLastSection`,然后手动设置各列宽度;或开启 `stretchLastSection` 让最后一列自行填充。 --- ### 任务 2.5: 编写 MainWindow 头文件 **产出文件:** `example/qwfassoc/src/mainwindow.hpp` ```cpp #pragma once #include "wfassoc++.h" #include #include #include #include #include #include #include #include #include namespace Ui { class MainWindow; } class MainWindow : public QDialog { Q_OBJECT public: explicit MainWindow( std::unique_ptr program, wfassoc::Scope scope, QWidget* parent = nullptr ); ~MainWindow() override; // 初始化窗口:调用 WFStartup,检查程序状态,填充 UI void Initialize(); private slots: void onInstallClicked(); void onUninstallClicked(); void onAssocCellClicked(int row, int column); void onSelectAllUserClicked(); void onSelectAllSystemClicked(); void onOkClicked(); void onCancelClicked(); void onApplyClicked(); private: // 初始化"应用程序"选项卡 void initAppTab(); // 刷新"应用程序"选项卡(根据注册状态启用/禁用按钮) void refreshAppTab(); // 初始化"文件关联"选项卡(首次填充表格) void initAssocTab(); // 刷新"文件关联"选项卡(重新查询所有扩展名状态并更新表格) void refreshAssocTab(); // 更新单行的显示(根据当前状态和待处理状态) void refreshAssocRow(int row); // 更新"应用"按钮的启用状态 void updateApplyButtonState(); // 判断在当前待处理状态下,某个 scope 的某个 ext 是否为 linked // 优先看 pending,再看实际状态 bool isExtLinked(wfassoc::Scope scope, size_t extIndex) const; // 判断当前程序是否已安装(使用给定的 scope) bool isProgramRegistered() const; // HICON 转 QPixmap 的工具函数 static QPixmap hiconToPixmap(void* hicon, int width = 32, int height = 32); // UI 元素(通过 UIC 自动生成,使用成员指针) std::unique_ptr ui_; // 核心数据 std::unique_ptr program_; wfassoc::Scope scope_; // 缓存的程序图标(用于在表格中表示"已关联到本程序"的状态) // 在初始化时获取,通过 Program::ResolveIcon() -> IconRc::GetIcon() 得到 HICON // 转换为 QPixmap 后缓存 QPixmap programIcon_; // 缓存的程序名称(用于判断某个 ExtStatus 是否属于本程序) std::string programName_; // 各个扩展名的内部信息 struct ExtInfo { std::string innerName; // 纯扩展名(不含点),如 "jpg" std::string dottedInnerName; // 带点的扩展名,如 ".jpg" }; std::vector extInfos_; // 索引对应 WFProgramGetExt 的 index // 待处理的链接/取消链接操作 // 键:extIndex // 值:true=待链接(link), false=待取消链接(unlink) // 如果某 extIndex 在此 map 中无记录,则无待处理操作 std::map pendingUserOps_; // 用户视图的待处理操作 std::map pendingSystemOps_; // 系统视图的待处理操作 }; ``` --- ### 任务 2.6: 编写 MainWindow 实现文件 (mainwindow.cpp) 这是最复杂的部分,按功能区域逐步实现。 --- #### 2.6.1 构造函数 ```cpp MainWindow::MainWindow( std::unique_ptr program, wfassoc::Scope scope, QWidget* parent) : QDialog(parent) , ui_(std::make_unique()) , program_(std::move(program)) , scope_(scope) { ui_->setupUi(this); // 固定窗口大小 setFixedSize(480, 600); // 初始化 wfassoc 库 if (!wfassoc::WFStartup()) { // WFStartup 失败(此时无法调用 WFGetLastError) QMessageBox::critical(this, "错误", "无法初始化 wfassoc 库。"); // 抛出异常或标记失败,由 main.cpp 处理 throw std::runtime_error("WFStartup failed"); } // 解析程序名称并设置窗口标题 programName_ = program_->ResolveName(); setWindowTitle(QString::fromStdString(programName_) + "选项"); // 解析程序图标并设置窗口图标 + 应用程序选项卡图标 + 表格占位图标 auto iconRc = program_->ResolveIcon(); if (iconRc) { void* hicon = iconRc->GetIcon(); QPixmap pixmap = hiconToPixmap(hicon, 48, 48); if (!pixmap.isNull()) { programIcon_ = hiconToPixmap(hicon, 32, 32); // 表格中用小图标 } setWindowIcon(QIcon(pixmap)); } // 遍历扩展名列表,缓存 extInfo size_t extCount = program_->ExtsLen(); extInfos_.reserve(extCount); for (size_t i = 0; i < extCount; ++i) { auto ext = program_->GetExt(i); extInfos_.push_back({ ext->GetInner(), ext->GetDottedInner() }); } // 连接信号与槽 connect(ui_->btnInstall, &QPushButton::clicked, this, &MainWindow::onInstallClicked); connect(ui_->btnUninstall, &QPushButton::clicked, this, &MainWindow::onUninstallClicked); connect(ui_->tableAssoc, &QTableWidget::cellClicked, this, &MainWindow::onAssocCellClicked); connect(ui_->btnSelectAllUser, &QPushButton::clicked, this, &MainWindow::onSelectAllUserClicked); connect(ui_->btnSelectAllSystem, &QPushButton::clicked, this, &MainWindow::onSelectAllSystemClicked); connect(ui_->btnOk, &QPushButton::clicked, this, &MainWindow::onOkClicked); connect(ui_->btnCancel, &QPushButton::clicked, this, &MainWindow::onCancelClicked); connect(ui_->btnApply, &QPushButton::clicked, this, &MainWindow::onApplyClicked); } ``` **析构函数:** ```cpp MainWindow::~MainWindow() { // program_ 先销毁(在其析构中调用 WFProgramDestroy) // 然后调用 WFShutdown 清理库 program_.reset(); wfassoc::WFShutdown(); } ``` **WFStartup/WFShutdown 注意事项:** - `WFStartup` 在构造函数中调用。 - `WFShutdown` 在析构函数中调用。 - `program_` 必须在 `WFShutdown` 之前销毁,因为 Program 的析构函数会调用 `WFProgramDestroy`。 --- #### 2.6.2 HICON 到 QPixmap 的转换 ```cpp QPixmap MainWindow::hiconToPixmap(void* hicon, int width, int height) { // 使用 Qt6 的 QtWin::fromHICON // 需要 #include // 注意:QtWin 在 Qt6 中属于 QtGui 模块,需要链接 Qt6::Gui(Widgets 模块已经包含) QPixmap pixmap = QtWin::fromHICON(reinterpret_cast(hicon)); if (!pixmap.isNull() && width > 0 && height > 0) { pixmap = pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); } return pixmap; } ``` **注意:** `QtWin::fromHICON` 需要 `#include ` 头文件。此函数复制了 HICON 的数据,返回的 QPixmap 是独立的,不依赖原 HICON 的生命周期。 --- #### 2.6.3 Initialize() 方法 ```cpp void MainWindow::Initialize() { initAppTab(); initAssocTab(); refreshAppTab(); } ``` 按顺序初始化两个选项卡,然后根据注册状态刷新按钮可用性。 --- #### 2.6.4 "应用程序"选项卡实现 ##### initAppTab() 设置图标 Label 和文本 Label 的内容: ```cpp void MainWindow::initAppTab() { // 设置图标 auto iconRc = program_->ResolveIcon(); if (iconRc) { void* hicon = iconRc->GetIcon(); QPixmap pixmap = hiconToPixmap(hicon, 48, 48); if (!pixmap.isNull()) { ui_->lblAppIcon->setPixmap(pixmap); // 可能还需要设置 scaledContents 或 fixed size } } // 设置描述文本 QString desc = QString::fromStdString("在此安装或卸载" + programName_); ui_->lblAppDesc->setText(desc); } ``` ##### refreshAppTab() ```cpp void MainWindow::refreshAppTab() { bool registered = isProgramRegistered(); ui_->btnInstall->setEnabled(!registered); ui_->btnUninstall->setEnabled(registered); // 同时启用/禁用"文件关联"选项卡的整体内容 // 可使用 QTabWidget 的 setTabEnabled 或直接操作内部控件的 setEnabled QWidget* assocTab = ui_->tabWidget->widget(1); // 索引 1 是"文件关联"选项卡 assocTab->setEnabled(registered); } ``` ##### isProgramRegistered() ```cpp bool MainWindow::isProgramRegistered() const { try { return program_->IsRegistered(scope_); } catch (const std::runtime_error&) { // 查询失败时视为未注册 return false; } } ``` ##### onInstallClicked() ```cpp void MainWindow::onInstallClicked() { try { program_->Register(scope_); QMessageBox::information(this, "成功", "应用程序安装成功!"); refreshAppTab(); } catch (const std::runtime_error& e) { QMessageBox::critical(this, "错误", QString::fromStdString("安装失败:" + std::string(e.what()))); } } ``` ##### onUninstallClicked() ```cpp void MainWindow::onUninstallClicked() { try { program_->Unregister(scope_); QMessageBox::information(this, "成功", "应用程序卸载成功!"); refreshAppTab(); } catch (const std::runtime_error& e) { QMessageBox::critical(this, "错误", QString::fromStdString("卸载失败:" + std::string(e.what()))); } } ``` --- #### 2.6.5 "文件关联"选项卡实现 这是最复杂的部分。 ##### initAssocTab() ```cpp void MainWindow::initAssocTab() { // 设置描述标签 QString desc = QString::fromStdString("使用 " + programName_ + " 关联的文件类型:"); ui_->lblAssocDesc->setText(desc); // 设置表格列标题 // 列 1 标题设为当前用户名(标准格式,如 "DESKTOP-XXXX\username") QString userName = QString::fromLocal8Bit(qgetenv("USERNAME")); // 更完整的方式是使用 Windows API GetUserNameEx 或类似方法, // 但简化的用户名也足够 ui_->tableAssoc->setHorizontalHeaderItem(1, new QTableWidgetItem(userName)); // 列 0 标题 "类型" 和列 2 标题 "所有用户" 已在 UI 文件中设置 // 为表格添加行 size_t extCount = program_->ExtsLen(); ui_->tableAssoc->setRowCount(static_cast(extCount)); // 设置列宽 ui_->tableAssoc->setColumnWidth(0, 120); ui_->tableAssoc->setColumnWidth(1, 160); ui_->tableAssoc->setColumnWidth(2, 160); // 禁止编辑 ui_->tableAssoc->setEditTriggers(QAbstractItemView::NoEditTriggers); // 设置选择行为:选中单元格 ui_->tableAssoc->setSelectionBehavior(QAbstractItemView::SelectItems); // 初始刷新 refreshAssocTab(); // 设置"系统"列的可用性(如果 scope 是 User,则系统列不可点击) if (scope_ == wfassoc::Scope::User) { // 禁用第 2 列的交互(仍可显示,但不能点击) for (int row = 0; row < static_cast(extCount); ++row) { QTableWidgetItem* item = ui_->tableAssoc->item(row, 2); if (!item) { item = new QTableWidgetItem(); ui_->tableAssoc->setItem(row, 2, item); } item->setFlags(item->flags() & ~Qt::ItemIsEnabled); } } } ``` ##### refreshAssocTab() 完全重新查询所有扩展名的状态,覆盖式刷新整个表格: ```cpp void MainWindow::refreshAssocTab() { size_t extCount = program_->ExtsLen(); for (size_t i = 0; i < extCount; ++i) { refreshAssocRow(static_cast(i)); } updateApplyButtonState(); } ``` ##### refreshAssocRow(int row) 单行刷新逻辑: ```cpp void MainWindow::refreshAssocRow(int row) { size_t extIndex = static_cast(row); // --- 列 0:类型(扩展名 + 图标) --- // 先查询 Hybrid 视图获取图标 // 然后根据用户/系统视图是否关联来决定最终显示的图标 auto hybridStatus = program_->QueryExt(wfassoc::View::Hybrid, extIndex); HICON hybridIcon = nullptr; if (hybridStatus) { hybridIcon = hybridStatus->GetIcon(); } // 判断当前(包含 pending)的用户和系统是否链接到本程序 bool userLinked = isExtLinked(wfassoc::Scope::User, extIndex); bool systemLinked = isExtLinked(wfassoc::Scope::System, extIndex); QTableWidgetItem* typeItem = ui_->tableAssoc->item(row, 0); if (!typeItem) { typeItem = new QTableWidgetItem(); ui_->tableAssoc->setItem(row, 0, typeItem); } // 设置文本:扩展名 typeItem->setText(QString::fromStdString(extInfos_[extIndex].dottedInnerName)); // 设置图标: // - 如果 user 或 system 任一 pending/actual 是 linked → 显示本程序图标 // - 否则如果 hybrid 有图标 → 显示 hybrid 图标 // - 否则空白 if (userLinked || systemLinked) { if (!programIcon_.isNull()) { typeItem->setIcon(QIcon(programIcon_)); } } else if (hybridIcon != nullptr) { QPixmap pixmap = hiconToPixmap(hybridIcon); if (!pixmap.isNull()) { typeItem->setIcon(QIcon(pixmap)); } else { typeItem->setIcon(QIcon()); } } else { typeItem->setIcon(QIcon()); } typeItem->setFlags(typeItem->flags() & ~Qt::ItemIsEditable); // --- 列 1:用户视图状态 --- auto userStatus = program_->QueryExt(wfassoc::View::User, extIndex); std::string userName; if (userStatus) { userName = userStatus->GetName(); } QTableWidgetItem* userItem = ui_->tableAssoc->item(row, 1); if (!userItem) { userItem = new QTableWidgetItem(); ui_->tableAssoc->setItem(row, 1, userItem); } // 如果有 pending 操作,则 pending 优先显示 auto pendingUserIt = pendingUserOps_.find(extIndex); if (pendingUserIt != pendingUserOps_.end()) { // pending link → 显示本程序名 // pending unlink → 显示空白 userItem->setText(pendingUserIt->second ? QString::fromStdString(programName_) : QString()); } else { // 无 pending:如果状态名等于本程序名,说明关联到了本程序 // 否则显示实际关联的程序名(或空白) if (!userName.empty() && userName == programName_) { userItem->setText(QString::fromStdString(programName_)); } else { userItem->setText(QString::fromStdString(userName)); } } userItem->setFlags((userItem->flags() | Qt::ItemIsEnabled) & ~Qt::ItemIsEditable); // --- 列 2:系统视图状态 --- auto systemStatus = program_->QueryExt(wfassoc::View::System, extIndex); std::string systemName; if (systemStatus) { systemName = systemStatus->GetName(); } QTableWidgetItem* systemItem = ui_->tableAssoc->item(row, 2); if (!systemItem) { systemItem = new QTableWidgetItem(); ui_->tableAssoc->setItem(row, 2, systemItem); } auto pendingSysIt = pendingSystemOps_.find(extIndex); if (pendingSysIt != pendingSystemOps_.end()) { systemItem->setText(pendingSysIt->second ? QString::fromStdString(programName_) : QString()); } else { if (!systemName.empty() && systemName == programName_) { systemItem->setText(QString::fromStdString(programName_)); } else { systemItem->setText(QString::fromStdString(systemName)); } } // 如果是 User scope 模式,系统列不可交互 if (scope_ == wfassoc::Scope::User) { systemItem->setFlags((systemItem->flags() & ~Qt::ItemIsEnabled) & ~Qt::ItemIsEditable); } else { systemItem->setFlags((systemItem->flags() | Qt::ItemIsEnabled) & ~Qt::ItemIsEditable); } } ``` ##### isExtLinked(size_t scope, size_t extIndex) ```cpp bool MainWindow::isExtLinked(wfassoc::Scope scope, size_t extIndex) const { // 先检查 pending 状态 auto& pendingMap = (scope == wfassoc::Scope::User) ? pendingUserOps_ : pendingSystemOps_; auto it = pendingMap.find(extIndex); if (it != pendingMap.end()) { return it->second; // pending link → true, pending unlink → false } // 否则查询实际状态 auto view = (scope == wfassoc::Scope::User) ? wfassoc::View::User : wfassoc::View::System; auto status = program_->QueryExt(view, extIndex); if (status) { std::string name = status->GetName(); return !name.empty() && name == programName_; } return false; } ``` ##### onAssocCellClicked(int row, int column) ```cpp void MainWindow::onAssocCellClicked(int row, int column) { // 只处理列 1(用户视图)和列 2(系统视图) if (column != 1 && column != 2) { return; } // 如果 scope 是 User 且点击的是系统列,忽略 if (column == 2 && scope_ == wfassoc::Scope::User) { return; } wfassoc::Scope targetScope = (column == 1) ? wfassoc::Scope::User : wfassoc::Scope::System; size_t extIndex = static_cast(row); bool currentlyLinked = isExtLinked(targetScope, extIndex); auto& pendingMap = (targetScope == wfassoc::Scope::User) ? pendingUserOps_ : pendingSystemOps_; if (currentlyLinked) { // 当前已链接 → 待取消链接 pendingMap[extIndex] = false; // false = pending unlink } else { // 当前未链接 → 待链接 pendingMap[extIndex] = true; // true = pending link } // 刷新该行 refreshAssocRow(row); updateApplyButtonState(); } ``` ##### onSelectAllUserClicked() 全选按钮操作("+"按钮,作用域 = User): ```cpp void MainWindow::onSelectAllUserClicked() { onSelectAllWithScope(wfassoc::Scope::User); } void MainWindow::onSelectAllSystemClicked() { // 如果 scope 是 User,此按钮不应起作用 if (scope_ == wfassoc::Scope::User) { return; } onSelectAllWithScope(wfassoc::Scope::System); } // 内部全选逻辑 void MainWindow::onSelectAllWithScope(wfassoc::Scope targetScope) { auto& pendingMap = (targetScope == wfassoc::Scope::User) ? pendingUserOps_ : pendingSystemOps_; // 第一阶段:检查是否还有未关联的项(显示为空白的行) bool allLinked = true; size_t extCount = program_->ExtsLen(); for (size_t i = 0; i < extCount; ++i) { if (!isExtLinked(targetScope, i)) { allLinked = false; break; } } if (!allLinked) { // 有未关联的项 → 将所有未关联的设置为 link for (size_t i = 0; i < extCount; ++i) { if (!isExtLinked(targetScope, i)) { pendingMap[i] = true; } } } else { // 所有项都已关联 → 将所有项全部设为 link(第二次点击) for (size_t i = 0; i < extCount; ++i) { pendingMap[i] = true; } } // 刷新整个表格 refreshAssocTab(); } ``` **注意:** 需要在 `mainwindow.hpp` 中补充声明 `void onSelectAllWithScope(wfassoc::Scope targetScope);`。 ##### onOkClicked() ```cpp void MainWindow::onOkClicked() { // 应用所有待处理的修改 applyPendingChanges(); accept(); // 关闭对话框(QDialog::accept()) } ``` ##### onCancelClicked() ```cpp void MainWindow::onCancelClicked() { // 丢弃所有待处理修改,直接关闭 reject(); // QDialog::reject() } ``` ##### onApplyClicked() ```cpp void MainWindow::onApplyClicked() { applyPendingChanges(); // 刷新视图(清除 pending 状态,重新查询) pendingUserOps_.clear(); pendingSystemOps_.clear(); refreshAssocTab(); } ``` ##### applyPendingChanges() 实际执行注册表修改的私有方法: ```cpp void MainWindow::applyPendingChanges() { // 应用用户视图的待处理操作 for (const auto& [extIndex, link] : pendingUserOps_) { try { if (link) { program_->LinkExt(wfassoc::Scope::User, extIndex); } else { program_->UnlinkExt(wfassoc::Scope::User, extIndex); } } catch (const std::runtime_error& e) { QMessageBox::critical(this, "错误", QString::fromStdString("操作文件扩展名 " + extInfos_[extIndex].dottedInnerName + " 失败:" + std::string(e.what()))); } } // 应用系统视图的待处理操作 for (const auto& [extIndex, link] : pendingSystemOps_) { try { if (link) { program_->LinkExt(wfassoc::Scope::System, extIndex); } else { program_->UnlinkExt(wfassoc::Scope::System, extIndex); } } catch (const std::runtime_error& e) { QMessageBox::critical(this, "错误", QString::fromStdString("操作文件扩展名 " + extInfos_[extIndex].dottedInnerName + " 失败:" + std::string(e.what()))); } } } ``` ##### updateApplyButtonState() ```cpp void MainWindow::updateApplyButtonState() { bool hasPending = !pendingUserOps_.empty() || !pendingSystemOps_.empty(); ui_->btnApply->setEnabled(hasPending); } ``` --- ### 任务 2.7: 编写程序入口 main.cpp **产出文件:** `example/qwfassoc/src/main.cpp` ```cpp #include #include #include #include "mainwindow.hpp" #include "manifesto.hpp" #include "wfassoc++.h" int main(int argc, char* argv[]) { // --- 初始化 Qt --- // 设置 Application 属性(一些 Qt 平台需要,特别是 Windows) QApplication app(argc, argv); QApplication::setOrganizationName("wfassoc"); QApplication::setApplicationName("qwfassoc"); // 不设置默认窗口图标,将由程序动态获取 // --- 解析命令行 --- QCommandLineParser parser; parser.setApplicationDescription("wfassoc 可视化配置工具"); parser.addHelpOption(); // -c / --manifest:指定 TOML manifest 文件路径 QCommandLineOption manifestOption( QStringList() << "c" << "manifest", "指定要配置的应用程序的清单文件路径(TOML 格式)", "manifest_path" ); parser.addOption(manifestOption); // -f / --for:指定作用的范围(user / system) QCommandLineOption forOption( QStringList() << "f" << "for", "指定应用程序注册的范围:user(当前用户)或 system(所有用户)", "scope" ); parser.addOption(forOption); parser.process(app); // 检查必填选项 if (!parser.isSet(manifestOption)) { QMessageBox::critical(nullptr, "错误", "缺少命令行选项 -c/--manifest。\n请指定 manifest 文件路径。"); return 1; } if (!parser.isSet(forOption)) { QMessageBox::critical(nullptr, "错误", "缺少命令行选项 -f/--for。\n请指定作用范围:user 或 system。"); return 1; } // 解析 scope QString scopeStr = parser.value(forOption).trimmed().toLower(); wfassoc::Scope scope; if (scopeStr == "user") { scope = wfassoc::Scope::User; } else if (scopeStr == "system") { scope = wfassoc::Scope::System; } else { QMessageBox::critical(nullptr, "错误", QString("无效的 --for 参数值 '%1'。\n有效值:user 或 system。").arg(scopeStr)); return 1; } // --- 加载 manifest 并构建 Program --- std::unique_ptr program; try { std::string manifestPath = parser.value(manifestOption).toStdString(); program = BuildProgramFromManifest(manifestPath); } catch (const std::exception& e) { QMessageBox::critical(nullptr, "错误", QString::fromStdString("无法加载 manifest 文件:\n" + std::string(e.what()))); return 1; } // --- 创建并运行主窗口 --- try { MainWindow window(std::move(program), scope); window.Initialize(); window.exec(); // 模态显示对话框 } catch (const std::exception& e) { QMessageBox::critical(nullptr, "错误", QString::fromStdString("程序初始化失败:\n" + std::string(e.what()))); return 1; } return 0; } ``` **注意事项:** - QCommandLineParser 是 Qt 内置的命令行解析器(`#include `)。 - 所有错误条件(缺少参数、无效参数、manifest 解析失败、Program 创建失败等)都通过 QMessageBox::critical 弹出错误对话框,然后 `return 1` 退出程序。 - MainWindow 如果初始化失败,也会显示错误对话框并退出。 - MainWindow::exec() 进入 Qt 事件循环,直到对话框关闭。 --- ## 3. 数据流与状态模型总结 ### 3.1 程序启动流程 ``` main.cpp 解析命令行 ↓ BuildProgramFromManifest(tomlPath) ↓ 读取 TOML → 创建 Schema → 创建 Program ↓ 创建 MainWindow(Program, Scope) ↓ MainWindow::Initialize() ├── WFStartup() // 初始化 wfassoc 库 ├── Program::ResolveName() // 获取程序名称 → 设置窗口标题 ├── Program::ResolveIcon() // 获取程序图标 → 设置窗口图标 + 缓存 ├── Program::ExtsLen() + GetExt() // 遍历扩展名列表 → 缓存 extInfos_ ├── initAppTab() // 设置"应用程序"选项卡内容 │ └── 调用 RefreshAppTab() // 根据 isRegistered 设置按钮状态 └── initAssocTab() // 设置"文件关联"选项卡内容 └── refreshAssocTab() // 遍历所有扩展名 → QueryExt → 填充表格 ``` ### 3.2 安装/卸载操作流程 ``` 用户点击"安装" / "卸载" ↓ Program::Register(scope) / Program::Unregister(scope) ↓ 成功 QMessageBox 显示成功 → refreshAppTab() ↓ 失败 QMessageBox 显示错误(含 WFGetLastError 的信息) ``` ### 3.3 文件关联操作流程 ``` 用户点击表格单元格(列 1 或列 2) ↓ 判断当前状态(pending优先,否则实际状态) ↓ 如果当前已关联 → 设置 pending = unlink 如果当前未关联 → 设置 pending = link ↓ refreshAssocRow(row) → 更新显示 updateApplyButtonState() → 启用/禁用"应用"按钮 ``` ### 3.4 应用/确定操作流程 ``` 用户点击"应用"或"确定" ↓ 遍历 pendingUserOps_ → 逐个调用 LinkExt/UnlinkExt 遍历 pendingSystemOps_ → 逐个调用 LinkExt/UnlinkExt ↓ 清空 pending maps ↓ 如果是"确定" → accept() 关闭对话框 如果是"应用" → refreshAssocTab() 重新查询显示 ``` ### 3.5 状态机说明 表格中每个单元格(行=扩展名,列=作用域)有三种可能的状态: | 实际注册表状态 | Pending 状态 | 显示内容 | 图标 | |---|---|---|---| | 关联到本程序 | 无 pending | 本程序名 | 本程序图标 | | 关联到本程序 | pending unlink | 空白 | 空白(如果没有另一个scope已关联) | | 未关联/关联到其他 | 无 pending | 空白/其他程序名 | Hybrid 图标或空白 | | 未关联/关联到其他 | pending link | 本程序名 | 本程序图标 | --- ## 4. 额外注意事项 ### 4.1 用户名获取 为获取列 1 的表头用户名,可以使用以下方式(按推荐顺序): 1. 通过 `GetUserNameExW` Windows API 获取完整用户名(格式:`DOMAIN\username` 或 `COMPUTERNAME\username`) 2. 通过环境变量 `USERNAME` 获取简单用户名(仅 `username`) 推荐使用方法 1,因为与 Windows 资源管理器中显示的格式一致。实现代码示例如下: ```cpp #include #include QString getFullUserName() { WCHAR buf[256]; ULONG size = 256; if (GetUserNameExW(NameSamCompatible, buf, &size)) { return QString::fromWCharArray(buf); } return QString::fromLocal8Bit(qgetenv("USERNAME")); } ``` 使用 `NameSamCompatible` 格式可得到如 `DESKTOP-XXXX\username` 的完整用户名。 ### 4.2 图标生命周期 `IconRc` 对象的 `GetIcon()` 返回的 HICON 指针仅在 `IconRc` 对象存活期间有效。必须先通过 `QtWin::fromHICON` 转换为 `QPixmap`(该函数会复制图标数据),然后缓存 `QPixmap` 而非 HICON 句柄。 `ExtStatus` 的 `GetIcon()` 同理,仅在 `ExtStatus` 对象存活期间有效。在 `refreshAssocRow` 中使用完后就销毁了。 ### 4.3 Tab 页的索引 在 `.ui` 文件中,"应用程序"选项卡是索引 0,"文件关联"选项卡是索引 1。代码中通过以下方式获取: ```cpp QWidget* assocTab = ui_->tabWidget->widget(1); // 或者直接在 UI 文件中设置 objectName,如 tabApp, tabAssoc ``` ### 4.4 UI 安装选项卡中 QPushButton 的布局约束 安装和卸载按钮是 QHBoxLayout 中仅有的两个元素。如果希望按钮居左排列,可使用默认对齐方式。如果希望按钮之间有间距,可添加 Stretch。 ### 4.5 错误处理策略 - 命令行参数错误 → QMessageBox::critical → return 1 - Manifest 加载/解析/校检错误 → QMessageBox::critical → return 1 - WFStartup 失败 → QMessageBox::critical → 无法继续 - 操作过程中的错误(安装/卸载/链接/取消链接失败)→ QMessageBox::critical → 留在当前页面,不退出程序 ### 4.6 WFStartup / WFShutdown 调用时机 根据 `wfassoc.h` 的文档: - `WFStartup` 必须在 `MainWindow` 构造函数中调用(在创建 Program 之后,用户开始交互之前)。 - `WFShutdown` 在 `MainWindow` 析构函数中调用。 - **关键:** `program_` 的析构必须在 `WFShutdown()` 之前,因为 Program 的析构会调用 `WFProgramDestroy`。因此析构函数的实现应为: ```cpp MainWindow::~MainWindow() { program_.reset(); // 先销毁 Program(调用 WFProgramDestroy) wfassoc::WFShutdown(); // 再调用 WFShutdown 清理库 } ``` ### 4.7 字符串编码 - wfassoc 所有 API 使用 UTF-8 编码的字符串。 - Qt 默认在 QString 中使用 UTF-16。 - 使用 `QString::fromStdString(std::string)` 将 std::string(UTF-8)转换为 QString。 - 使用 `std::string s = qStr.toStdString()` 将 QString 转换为 std::string(UTF-8)。 - TOML 文件中的字符串也是 UTF-8 编码。 ### 4.8 禁用的列处理 - 当 scope 为 User 时,表格的第 2 列("所有用户")所有单元格的 `Qt::ItemIsEnabled` flag 被清除。 - 同时 `onAssocCellClicked` 中检查如果 `column == 2 && scope_ == User` 则直接 return。 - "全选"按钮中 "+" 按钮的行为了两个相同的——一个是针对 user 的,一个是针对 system 的。当 scope 为 User 时,system 的全选按钮也需要判断禁用(不执行任何操作)。 --- ## 5. 测试指导 实施完成后,建议通过以下场景测试: 1. **命令行参数测试**: - 缺少 -c 参数 → 应弹出错误对话框后退出 - 缺少 -f 参数 → 应弹出错误对话框后退出 - -f 参数无效 → 应弹出错误对话框后退出 - manifest 文件不存在 → 应弹出错误对话框后退出 - manifest 文件格式有误 → 应弹出错误对话框后退出 2. **"应用程序"选项卡测试**: - 程序未安装时:安装按钮可点击,卸载按钮禁用,文件关联选项卡全部禁用 - 点击安装 → 成功后按钮状态颠倒 - 点击卸载 → 成功后按钮状态颠倒 - 操作失败 → 弹出错误消息,保持当前状态 3. **"文件关联"选项卡测试**: - 程序未安装时:整个选项卡内容禁用 - 程序已安装时:可以正常交互 - 点击用户视图单元格 → 切换 pending 状态,图标更新 - 点击系统视图单元格 → 切换 pending 状态,图标更新 - User 模式下系统列单元格不可点击 - 点击"+"全选按钮 → 正确批量设置 - 有 pending 时"应用"按钮可点击,无 pending 时禁用 - 点击"应用" → 待处理操作执行,表格刷新 - 点击"确定" → 待处理操作执行,对话框关闭 - 点击"取消" → 待处理操作丢弃,对话框关闭