From 18a55272a312504d90229528a1e2a9d61411fb74 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Mon, 25 May 2026 23:26:11 +0800 Subject: [PATCH] ai: update tasks --- example/qwfassoc/PROMPT.txt | 11 +- example/qwfassoc/TASKS.md | 1313 ++++++++++++++++++++++ example/qwfassoc/cmake/Findwfassoc.cmake | 101 ++ example/qwfassoc/cmake/README.md | 5 + 4 files changed, 1429 insertions(+), 1 deletion(-) create mode 100644 example/qwfassoc/TASKS.md create mode 100644 example/qwfassoc/cmake/Findwfassoc.cmake create mode 100644 example/qwfassoc/cmake/README.md diff --git a/example/qwfassoc/PROMPT.txt b/example/qwfassoc/PROMPT.txt index 24111d2..ceb0570 100644 --- a/example/qwfassoc/PROMPT.txt +++ b/example/qwfassoc/PROMPT.txt @@ -118,7 +118,7 @@ - 不要尝试去编译来检查错误。我会安排其他人来检查程序是否能正常运行,并汇报回来,你再修改。 - 你不需要关心能否找到Qt,wfassoc和toml11这些库。 - 对于wfassoc,你只需要将@wfassoc-cdylib/codegen/Findwfassoc.cmake 复制到@example/qwfassoc/cmake 目录下,并在此目录下编写一个README.md,表明这个文件是从哪里复制来的即可。然后把复制的cmake文件所在目录加入find_package目录,然后使用find_package寻找wfassoc即可。至于去哪里找这个库,我会安排其他人来做。 - - 对于Qt和toml11我会安排其他人来做,你只需要用find_package来找他们就行,需要操心能不能找到。 + - 对于Qt和toml11我会安排其他人来做,你只需要用find_package来找他们就行,不需要操心能不能找到。 - 如果你对某项需求有疑问,请问我,而不是进行猜测。 @@ -230,3 +230,12 @@ mainLayout->addLayout(bottomLayout); * "确定" * "取消" * "应用(A)" (注意:截图中该按钮呈灰色,代码中需设置 `setEnabled(false)`)。 + + + +你制定的计划有一些问题,请按照下述标出的问题一一修正: + +- 编写的代码和说明文件需要使用英文注释。 +- 是manifest而非manifesto,表示清单文件,请修正这个拼写错误。 +- 我看到你在mainwindow篇章中编写了大量的C++代码,这没有必要。你是计划者而非执行者。你需要把需要在这个头文件中实现什么?该怎么做?需要使用哪些wfassoc函数,这些函数该怎么调用?在哪里查看他们怎么调用?详细的告诉将要执行这些任务的执行者,而不是直接为他们编写好代码。你在manifest部分的任务规划就非常符合这种范式。 + diff --git a/example/qwfassoc/TASKS.md b/example/qwfassoc/TASKS.md new file mode 100644 index 0000000..07bee8b --- /dev/null +++ b/example/qwfassoc/TASKS.md @@ -0,0 +1,1313 @@ +# 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 时禁用 + - 点击"应用" → 待处理操作执行,表格刷新 + - 点击"确定" → 待处理操作执行,对话框关闭 + - 点击"取消" → 待处理操作丢弃,对话框关闭 diff --git a/example/qwfassoc/cmake/Findwfassoc.cmake b/example/qwfassoc/cmake/Findwfassoc.cmake new file mode 100644 index 0000000..b2ee06d --- /dev/null +++ b/example/qwfassoc/cmake/Findwfassoc.cmake @@ -0,0 +1,101 @@ +# Findwfassoc.cmake +# ---------------- +# Find wfassoc library and headers. +# +# This module requires the user to set wfassoc_ROOT to the installation +# directory of wfassoc. The directory structure under wfassoc_ROOT must be: +# bin/ - contains wfassoc_cdylib.dll +# include/ - contains wfassoc.h and wfassoc++.h +# lib/ - contains wfassoc_cdylib.dll.lib (import library) +# +# This module defines the following variables: +# wfassoc_FOUND - True if wfassoc was found +# wfassoc_INCLUDE_DIRS - Path to wfassoc include directory +# wfassoc_LIBRARIES - Path to wfassoc import library +# wfassoc_DLL - Path to wfassoc DLL +# wfassoc_ROOT - The root directory (user-provided) +# +# This module also creates the following imported targets: +# wfassoc::wfassoc - Main wfassoc library (includes both include and link) +# + +set(wfassoc_FOUND FALSE) + +# Require user to set wfassoc_ROOT +if(NOT wfassoc_ROOT) + message(FATAL_ERROR "wfassoc_ROOT must be set to the installation directory of wfassoc") +endif() + +# Check existence of required subdirectories +if(NOT EXISTS ${wfassoc_ROOT}) + message(FATAL_ERROR "wfassoc_ROOT directory does not exist: ${wfassoc_ROOT}") +endif() + +set(wfassoc_INCLUDE_DIR ${wfassoc_ROOT}/include) +set(wfassoc_LIB_DIR ${wfassoc_ROOT}/lib) +set(wfassoc_BIN_DIR ${wfassoc_ROOT}/bin) + +# Find header files +if(EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc.h AND EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc++.h) + set(wfassoc_INCLUDE_DIRS ${wfassoc_INCLUDE_DIR}) +else() + message(SEND_ERROR "Missing wfassoc header files in ${wfassoc_INCLUDE_DIR}") + return() +endif() + +# Find import library (.lib) +find_file(wfassoc_LIBRARIES + NAMES wfassoc_cdylib.dll.lib + PATHS ${wfassoc_LIB_DIR} + NO_DEFAULT_PATH + DOC "wfassoc import library" +) + +if(NOT wfassoc_LIBRARIES) + message(SEND_ERROR "Missing wfassoc import library (wfassoc_cdylib.dll.lib) in ${wfassoc_LIB_DIR}") + return() +endif() + +# Find DLL file +find_file(wfassoc_DLL + NAMES wfassoc_cdylib.dll + PATHS ${wfassoc_BIN_DIR} + NO_DEFAULT_PATH + DOC "wfassoc dynamic library" +) + +if(NOT wfassoc_DLL) + message(SEND_ERROR "Missing wfassoc DLL (wfassoc_cdylib.dll) in ${wfassoc_BIN_DIR}") + return() +endif() + +# Everything found +set(wfassoc_FOUND TRUE) + +# Mark variables as advanced for ccmake/cmake-gui +mark_as_advanced(wfassoc_INCLUDE_DIRS wfassoc_LIBRARIES wfassoc_DLL) + +# Create imported target for wfassoc +if(wfassoc_FOUND AND NOT TARGET wfassoc::wfassoc) + add_library(wfassoc::wfassoc SHARED IMPORTED) + + # Set include directories + set_target_properties(wfassoc::wfassoc PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${wfassoc_INCLUDE_DIRS} + ) + + # Set import library location + set_target_properties(wfassoc::wfassoc PROPERTIES + IMPORTED_IMPLIB "${wfassoc_LIBRARIES}" + IMPORTED_LOCATION "${wfassoc_DLL}" + ) +endif() + +# Optional: Print status message +if(wfassoc_FOUND) + message(STATUS "Found wfassoc:") + message(STATUS " Root : ${wfassoc_ROOT}") + message(STATUS " Include : ${wfassoc_INCLUDE_DIRS}") + message(STATUS " Library : ${wfassoc_LIBRARIES}") + message(STATUS " DLL : ${wfassoc_DLL}") +endif() diff --git a/example/qwfassoc/cmake/README.md b/example/qwfassoc/cmake/README.md new file mode 100644 index 0000000..822b433 --- /dev/null +++ b/example/qwfassoc/cmake/README.md @@ -0,0 +1,5 @@ +# cmake 模块说明 + +此目录下的 `Findwfassoc.cmake` 是从项目根目录 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制而来。 + +该文件提供 `wfassoc::wfassoc` imported target。使用前需要设置 `wfassoc_ROOT` 变量指向 wfassoc 安装目录。