这是一份 Qt Widgets 文件菜单示例,整理自尚未接入项目的代码草稿。菜单提供“打开、删除、属性”三个动作;示例只浏览程序临时创建的演示目录,便于试用。它不包含整套文件管理器的访问控制、回收站或并发文件事务。
原代码的“属性”动作没有实现,删除失败没有反馈,还把短期有效的 QModelIndex 捕获到可能执行很久的交互中。下面补齐这些边界,并使用非阻塞菜单和确认框,避免 exec() 引入的嵌套事件循环。
从鼠标位置到一个可重新核对的目标
跳转到“从鼠标位置到一个可重新核对的目标”QTreeView 继承 QAbstractScrollArea,其上下文菜单位置按视口坐标处理:indexAt(pos) 取得点击项,viewport()->mapToGlobal(pos) 将同一位置转换为菜单的屏幕坐标。如果视图前面还有代理模型,必须先映射回文件模型;本例直接使用 QFileSystemModel,没有代理层。
交互期间模型可能排序、刷新或删除条目,因此保存 QPersistentModelIndex,执行动作前仍检查它有效、属于原模型且路径没有变化。QPointer 检查窗口/模型是否还活着;它们都不保证操作系统文件在检查后不会被另一个进程替换。持久索引、视口坐标
完整 Qt 6 / C++17 示例
跳转到“完整 Qt 6 / C++17 示例”QFileSystemModel 默认只读;示例主动允许修改,但删除动作只处理普通文件,不递归删除目录,也不处理符号链接。确认框默认选“否”,remove() 的失败会显示结果。该 API 直接删除文件,不会移到回收站,因此确认文本必须明确。QFileSystemModel
#include <QApplication>#include <QDesktopServices>#include <QFile>#include <QFileInfo>#include <QFileSystemModel>#include <QMenu>#include <QMessageBox>#include <QPersistentModelIndex>#include <QPointer>#include <QTemporaryDir>#include <QTreeView>#include <QUrl>
class FileBrowser : public QTreeView {public: explicit FileBrowser(const QString& root, QWidget* parent = nullptr) : QTreeView(parent), files_(new QFileSystemModel(this)) { files_->setReadOnly(false); setModel(files_); setRootIndex(files_->setRootPath(root)); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { if (auto* menu = makeMenu(indexAt(pos))) { menu->popup(viewport()->mapToGlobal(pos)); } }); }
QMenu* makeMenu(const QModelIndex& clicked) { if (!clicked.isValid() || clicked.model() != files_) return nullptr; const QPersistentModelIndex item(clicked.siblingAtColumn(0)); const QString originalPath = files_->filePath(item); const QPointer<FileBrowser> view(this); const QPointer<QFileSystemModel> files(files_); const auto valid = [view, files, item, originalPath] { return view && files && view->model() == files.data() && item.isValid() && item.model() == files.data() && files->filePath(item) == originalPath; };
auto* menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); auto* open = menu->addAction(QStringLiteral("打开")); auto* remove = menu->addAction(QStringLiteral("删除")); auto* properties = menu->addAction(QStringLiteral("属性")); open->setObjectName("open"); remove->setObjectName("remove"); properties->setObjectName("properties"); const QFileInfo info(originalPath); remove->setEnabled(!files_->isReadOnly() && info.isFile() && !info.isSymLink());
connect(open, &QAction::triggered, this, [view, files, item, valid] { if (!valid()) return; if (files->isDir(item)) { view->setRootIndex(item); } else if (!QDesktopServices::openUrl( QUrl::fromLocalFile(files->filePath(item)))) { view->inform(QStringLiteral("打开失败"), QStringLiteral("无法交给默认应用打开。")); } }); connect(properties, &QAction::triggered, this, [view, files, item, valid] { if (!valid()) return; const QFileInfo current(files->filePath(item)); view->inform(QStringLiteral("属性"), QStringLiteral("路径:%1\n类型:%2\n大小:%3 字节") .arg(current.absoluteFilePath()) .arg(current.isDir() ? QStringLiteral("目录") : QStringLiteral("文件")) .arg(current.size())); }); connect(remove, &QAction::triggered, this, [view, files, item, originalPath, valid] { if (!valid() || files->isReadOnly()) return; const QFileInfo current(originalPath); if (!current.isFile() || current.isSymLink()) return; auto* question = new QMessageBox(QMessageBox::Warning, QStringLiteral("永久删除"), QStringLiteral("直接删除以下文件,不进入回收站:\n%1").arg(originalPath), QMessageBox::Yes | QMessageBox::No, view.data()); question->setObjectName("delete-confirmation"); question->setTextFormat(Qt::PlainText); question->setDefaultButton(QMessageBox::No); connect(question, &QDialog::finished, question, &QObject::deleteLater); connect(question, &QDialog::finished, view.data(), [view, files, item, originalPath, valid](int answer) { if (answer != QMessageBox::Yes || !valid()) return; const QFileInfo latest(originalPath); if (files->isReadOnly() || !latest.isFile() || latest.isSymLink()) { view->inform(QStringLiteral("未删除"), QStringLiteral("目标或写入状态已变化。")); return; } if (!files->remove(item)) { view->inform(QStringLiteral("删除失败"), QStringLiteral("文件未能删除,请检查权限或占用状态。")); } }); question->open(); }); return menu; }
private: void inform(const QString& title, const QString& text) { auto* message = new QMessageBox(QMessageBox::Information, title, text, QMessageBox::Ok, this); message->setTextFormat(Qt::PlainText); connect(message, &QDialog::finished, message, &QObject::deleteLater); message->open(); } QFileSystemModel* files_;};
int main(int argc, char** argv) { QApplication app(argc, argv); QTemporaryDir directory; // 退出时仅清理本例自己的临时目录 if (!directory.isValid()) return 1; QFile sample(directory.filePath("sample.txt")); if (!sample.open(QIODevice::WriteOnly)) return 2; if (sample.write("temporary demo\n") < 0) return 3; sample.close(); FileBrowser browser(directory.path()); browser.resize(700, 400); browser.setWindowTitle(QStringLiteral("临时目录文件菜单示例")); browser.show(); return app.exec();}构建时使用 Qt Widgets:
cmake_minimum_required(VERSION 3.20)project(FileMenu LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 REQUIRED COMPONENTS Widgets)add_executable(file-menu main.cpp)target_link_libraries(file-menu PRIVATE Qt6::Widgets)本例未声明自定义信号或元对象成员,不需要 Q_OBJECT。QMenu、确认框和消息框由浏览器窗口拥有;关闭时释放交互对象,窗口销毁时 Qt 会断开以它为上下文的连接。
示例边界与验证
跳转到“示例边界与验证”示例在 Qt 6.10.2 编译,并在隔离临时目录验证了菜单属性、目录打开、删除取消/确认、只读状态变化和目标失效,不对真实资料执行删除。打开普通文件会调用系统默认应用,因此自动验证不触发这个外部动作。
setRootPath 和视图 setRootIndex 只是模型加载/展示设置,不是文件访问沙箱。本例的菜单从当前视图产生,但如果扩展了搜索、拖放、路径输入等入口,必须根据产品要求另外限制允许操作的目录。这里也不把路径字符串相同当成文件身份未变化的保证;并发替换、跨进程竞争以及强身份校验属于更完整文件管理器需要解决的问题。
原稿见 CSDN 文件菜单草稿。这里保留三个原始动作,并补上可执行实现与失败边界,归入 Qt 模型/视图分类。