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

44 KiB
Raw Permalink Blame History

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 中写入以下内容(纯文本说明文件):
# cmake 模块说明

此目录下的 `Findwfassoc.cmake` 是从项目根目录 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制而来。

该文件提供 `wfassoc::wfassoc` imported target。使用前需要设置 `wfassoc_ROOT` 变量指向 wfassoc 安装目录。

任务 2.2: 编写 CMakeLists.txt

产出文件: example/qwfassoc/CMakeLists.txt

内容要求:

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

头文件内容规范:

#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 读取语法示例:

    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");
    

    可选字段读取:

    std::string name;
    bool has_name = data.contains("name");
    if (has_name) {
        name = toml::find<std::string>(data, "name");
    }
    

    icon、behavior 同理)

    表格读取:

    const auto& strs = toml::find(data, "strs").as_table();
    // 遍历 strs: for (const auto& [key, value] : strs) { ... value.as_string() ... }
    

    exts 表格读取(每个值是嵌套表,包含 name、icon、behavior 字段):

    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 对象并填充字段:

    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

    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

#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 构造函数

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);
}

析构函数:

MainWindow::~MainWindow() {
    // program_ 先销毁(在其析构中调用 WFProgramDestroy
    // 然后调用 WFShutdown 清理库
    program_.reset();
    wfassoc::WFShutdown();
}

WFStartup/WFShutdown 注意事项:

  • WFStartup 在构造函数中调用。
  • WFShutdown 在析构函数中调用。
  • program_ 必须在 WFShutdown 之前销毁,因为 Program 的析构函数会调用 WFProgramDestroy

2.6.2 HICON 到 QPixmap 的转换

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() 方法

void MainWindow::Initialize() {
    initAppTab();
    initAssocTab();
    refreshAppTab();
}

按顺序初始化两个选项卡,然后根据注册状态刷新按钮可用性。


2.6.4 "应用程序"选项卡实现

initAppTab()

设置图标 Label 和文本 Label 的内容:

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()
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()
bool MainWindow::isProgramRegistered() const {
    try {
        return program_->IsRegistered(scope_);
    } catch (const std::runtime_error&) {
        // 查询失败时视为未注册
        return false;
    }
}
onInstallClicked()
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()
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()
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()

完全重新查询所有扩展名的状态,覆盖式刷新整个表格:

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)

单行刷新逻辑:

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)
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)
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

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()
void MainWindow::onOkClicked() {
    // 应用所有待处理的修改
    applyPendingChanges();
    accept();  // 关闭对话框QDialog::accept()
}
onCancelClicked()
void MainWindow::onCancelClicked() {
    // 丢弃所有待处理修改,直接关闭
    reject();  // QDialog::reject()
}
onApplyClicked()
void MainWindow::onApplyClicked() {
    applyPendingChanges();
    // 刷新视图(清除 pending 状态,重新查询)
    pendingUserOps_.clear();
    pendingSystemOps_.clear();
    refreshAssocTab();
}
applyPendingChanges()

实际执行注册表修改的私有方法:

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()
void MainWindow::updateApplyButtonState() {
    bool hasPending = !pendingUserOps_.empty() || !pendingSystemOps_.empty();
    ui_->btnApply->setEnabled(hasPending);
}

任务 2.7: 编写程序入口 main.cpp

产出文件: example/qwfassoc/src/main.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\usernameCOMPUTERNAME\username
  2. 通过环境变量 USERNAME 获取简单用户名(仅 username

推荐使用方法 1因为与 Windows 资源管理器中显示的格式一致。实现代码示例如下:

#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 句柄。

ExtStatusGetIcon() 同理,仅在 ExtStatus 对象存活期间有效。在 refreshAssocRow 中使用完后就销毁了。

4.3 Tab 页的索引

.ui 文件中,"应用程序"选项卡是索引 0"文件关联"选项卡是索引 1。代码中通过以下方式获取

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 之后,用户开始交互之前)。
  • WFShutdownMainWindow 析构函数中调用。
  • 关键: program_ 的析构必须在 WFShutdown() 之前,因为 Program 的析构会调用 WFProgramDestroy。因此析构函数的实现应为:
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 时禁用
    • 点击"应用" → 待处理操作执行,表格刷新
    • 点击"确定" → 待处理操作执行,对话框关闭
    • 点击"取消" → 待处理操作丢弃,对话框关闭