44 KiB
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 模块文件
操作内容:
- 将
wfassoc-cdylib/codegen/Findwfassoc.cmake复制到example/qwfassoc/cmake/Findwfassoc.cmake。 - 在
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
实现流程:
-
使用
toml::parse(tomlPath)解析 TOML 文件。如果解析失败,toml11 会抛出异常(通常是toml::syntax_error)。直接让异常向上传播。 -
从解析结果中提取数据:
- 必填字段:
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 的文档。 - 必填字段:
-
创建
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()); } -
用 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,高度 600sizePolicy: 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:SelectItemseditTriggers: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::Gui(Widgets 模块已经包含)
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 的表头用户名,可以使用以下方式(按推荐顺序):
- 通过
GetUserNameExWWindows API 获取完整用户名(格式:DOMAIN\username或COMPUTERNAME\username) - 通过环境变量
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 句柄。
ExtStatus 的 GetIcon() 同理,仅在 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 之后,用户开始交互之前)。WFShutdown在MainWindow析构函数中调用。- 关键:
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::string(UTF-8)转换为 QString。 - 使用
std::string s = qStr.toStdString()将 QString 转换为 std::string(UTF-8)。 - TOML 文件中的字符串也是 UTF-8 编码。
4.8 禁用的列处理
- 当 scope 为 User 时,表格的第 2 列("所有用户")所有单元格的
Qt::ItemIsEnabledflag 被清除。 - 同时
onAssocCellClicked中检查如果column == 2 && scope_ == User则直接 return。 - "全选"按钮中 "+" 按钮的行为了两个相同的——一个是针对 user 的,一个是针对 system 的。当 scope 为 User 时,system 的全选按钮也需要判断禁用(不执行任何操作)。
5. 测试指导
实施完成后,建议通过以下场景测试:
-
命令行参数测试:
- 缺少 -c 参数 → 应弹出错误对话框后退出
- 缺少 -f 参数 → 应弹出错误对话框后退出
- -f 参数无效 → 应弹出错误对话框后退出
- manifest 文件不存在 → 应弹出错误对话框后退出
- manifest 文件格式有误 → 应弹出错误对话框后退出
-
"应用程序"选项卡测试:
- 程序未安装时:安装按钮可点击,卸载按钮禁用,文件关联选项卡全部禁用
- 点击安装 → 成功后按钮状态颠倒
- 点击卸载 → 成功后按钮状态颠倒
- 操作失败 → 弹出错误消息,保持当前状态
-
"文件关联"选项卡测试:
- 程序未安装时:整个选项卡内容禁用
- 程序已安装时:可以正常交互
- 点击用户视图单元格 → 切换 pending 状态,图标更新
- 点击系统视图单元格 → 切换 pending 状态,图标更新
- User 模式下系统列单元格不可点击
- 点击"+"全选按钮 → 正确批量设置
- 有 pending 时"应用"按钮可点击,无 pending 时禁用
- 点击"应用" → 待处理操作执行,表格刷新
- 点击"确定" → 待处理操作执行,对话框关闭
- 点击"取消" → 待处理操作丢弃,对话框关闭