1
0
Files
wfassoc/example/qwfassoc/TASKS.md
2026-05-25 23:26:11 +08:00

1314 lines
44 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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}"
$<TARGET_FILE_DIR:qwfassoc>
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 <memory>
#include <string>
// 从 TOML manifest 文件构建 Program
// 内部完成 Schema 创建、字段填充、校检、Program 创建
// 如果任何步骤失败,抛出 std::runtime_error由 wfassoc++ 的 _Check 机制产生)
// 也可能抛出 toml11 的解析异常
std::unique_ptr<wfassocpp::Program> 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<std::string>(data, "identifier");
std::string path_val = toml::find<std::string>(data, "path");
std::string clsid = toml::find<std::string>(data, "clsid");
```
可选字段读取:
```cpp
std::string name;
bool has_name = data.contains("name");
if (has_name) {
name = toml::find<std::string>(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<std::string, std::shared_ptr<toml::value>>
// 但更简单的是在解析后使用 toml::find<std::string>(data, "exts") 然后再逐个读取
}
```
**toml11 访问注意事项:** 对于嵌套的 TOML 表(如 `[exts.jpg]`toml11 中使用 `toml::find` 时路径可以用点号拼接,例如 `toml::find<std::string>(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<std::string>(value, ""); // value 本身是字符串
// 实际上,遍历 as_table() 得到的 value 是 shared_ptr<toml::value>
// 需要用 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<wfassocpp::Program>(std::make_unique<wfassocpp::Schema>(std::move(schema)));
```
**注意:** Program 的构造函数接受 `std::unique_ptr<Schema>&&`。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 <QDialog>
#include <QTableWidget>
#include <QPushButton>
#include <QLabel>
#include <QTabWidget>
#include <memory>
#include <map>
#include <optional>
#include <vector>
namespace Ui {
class MainWindow;
}
class MainWindow : public QDialog {
Q_OBJECT
public:
explicit MainWindow(
std::unique_ptr<wfassocpp::Program> 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::MainWindow> ui_;
// 核心数据
std::unique_ptr<wfassocpp::Program> 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<ExtInfo> extInfos_; // 索引对应 WFProgramGetExt 的 index
// 待处理的链接/取消链接操作
// 键extIndex
// 值true=待链接(link), false=待取消链接(unlink)
// 如果某 extIndex 在此 map 中无记录,则无待处理操作
std::map<size_t, bool> pendingUserOps_; // 用户视图的待处理操作
std::map<size_t, bool> pendingSystemOps_; // 系统视图的待处理操作
};
```
---
### 任务 2.6: 编写 MainWindow 实现文件 (mainwindow.cpp)
这是最复杂的部分,按功能区域逐步实现。
---
#### 2.6.1 构造函数
```cpp
MainWindow::MainWindow(
std::unique_ptr<wfassocpp::Program> program,
wfassoc::Scope scope,
QWidget* parent)
: QDialog(parent)
, ui_(std::make_unique<Ui::MainWindow>())
, 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 <QtGui/QtWin>
// 注意QtWin 在 Qt6 中属于 QtGui 模块,需要链接 Qt6::GuiWidgets 模块已经包含)
QPixmap pixmap = QtWin::fromHICON(reinterpret_cast<HICON>(hicon));
if (!pixmap.isNull() && width > 0 && height > 0) {
pixmap = pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
return pixmap;
}
```
**注意:** `QtWin::fromHICON` 需要 `#include <QtGui/QtWin>` 头文件。此函数复制了 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<int>(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<int>(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<int>(i));
}
updateApplyButtonState();
}
```
##### refreshAssocRow(int row)
单行刷新逻辑:
```cpp
void MainWindow::refreshAssocRow(int row) {
size_t extIndex = static_cast<size_t>(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<size_t>(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 <QApplication>
#include <QCommandLineParser>
#include <QMessageBox>
#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<wfassocpp::Program> 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 <QCommandLineParser>`)。
- 所有错误条件缺少参数、无效参数、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 <windows.h>
#include <QString>
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::stringUTF-8转换为 QString。
- 使用 `std::string s = qStr.toStdString()` 将 QString 转换为 std::stringUTF-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 时禁用
- 点击"应用" → 待处理操作执行,表格刷新
- 点击"确定" → 待处理操作执行,对话框关闭
- 点击"取消" → 待处理操作丢弃,对话框关闭