1314 lines
44 KiB
Markdown
1314 lines
44 KiB
Markdown
# 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::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() 方法
|
||
|
||
```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::string(UTF-8)转换为 QString。
|
||
- 使用 `std::string s = qStr.toStdString()` 将 QString 转换为 std::string(UTF-8)。
|
||
- TOML 文件中的字符串也是 UTF-8 编码。
|
||
|
||
### 4.8 禁用的列处理
|
||
|
||
- 当 scope 为 User 时,表格的第 2 列("所有用户")所有单元格的 `Qt::ItemIsEnabled` flag 被清除。
|
||
- 同时 `onAssocCellClicked` 中检查如果 `column == 2 && scope_ == User` 则直接 return。
|
||
- "全选"按钮中 "+" 按钮的行为了两个相同的——一个是针对 user 的,一个是针对 system 的。当 scope 为 User 时,system 的全选按钮也需要判断禁用(不执行任何操作)。
|
||
|
||
---
|
||
|
||
## 5. 测试指导
|
||
|
||
实施完成后,建议通过以下场景测试:
|
||
|
||
1. **命令行参数测试**:
|
||
- 缺少 -c 参数 → 应弹出错误对话框后退出
|
||
- 缺少 -f 参数 → 应弹出错误对话框后退出
|
||
- -f 参数无效 → 应弹出错误对话框后退出
|
||
- manifest 文件不存在 → 应弹出错误对话框后退出
|
||
- manifest 文件格式有误 → 应弹出错误对话框后退出
|
||
|
||
2. **"应用程序"选项卡测试**:
|
||
- 程序未安装时:安装按钮可点击,卸载按钮禁用,文件关联选项卡全部禁用
|
||
- 点击安装 → 成功后按钮状态颠倒
|
||
- 点击卸载 → 成功后按钮状态颠倒
|
||
- 操作失败 → 弹出错误消息,保持当前状态
|
||
|
||
3. **"文件关联"选项卡测试**:
|
||
- 程序未安装时:整个选项卡内容禁用
|
||
- 程序已安装时:可以正常交互
|
||
- 点击用户视图单元格 → 切换 pending 状态,图标更新
|
||
- 点击系统视图单元格 → 切换 pending 状态,图标更新
|
||
- User 模式下系统列单元格不可点击
|
||
- 点击"+"全选按钮 → 正确批量设置
|
||
- 有 pending 时"应用"按钮可点击,无 pending 时禁用
|
||
- 点击"应用" → 待处理操作执行,表格刷新
|
||
- 点击"确定" → 待处理操作执行,对话框关闭
|
||
- 点击"取消" → 待处理操作丢弃,对话框关闭
|