跳转到内容
新建笔记

Qt 文件右键菜单:打开、属性与确认删除

这是一份 Qt Widgets 文件菜单示例,整理自尚未接入项目的代码草稿。菜单提供“打开、删除、属性”三个动作;示例只浏览程序临时创建的演示目录,便于试用。它不包含整套文件管理器的访问控制、回收站或并发文件事务。

原代码的“属性”动作没有实现,删除失败没有反馈,还把短期有效的 QModelIndex 捕获到可能执行很久的交互中。下面补齐这些边界,并使用非阻塞菜单和确认框,避免 exec() 引入的嵌套事件循环。

从鼠标位置到一个可重新核对的目标

跳转到“从鼠标位置到一个可重新核对的目标”

QTreeView 继承 QAbstractScrollArea,其上下文菜单位置按视口坐标处理:indexAt(pos) 取得点击项,viewport()->mapToGlobal(pos) 将同一位置转换为菜单的屏幕坐标。如果视图前面还有代理模型,必须先映射回文件模型;本例直接使用 QFileSystemModel,没有代理层。

交互期间模型可能排序、刷新或删除条目,因此保存 QPersistentModelIndex,执行动作前仍检查它有效、属于原模型且路径没有变化。QPointer 检查窗口/模型是否还活着;它们都不保证操作系统文件在检查后不会被另一个进程替换。持久索引、视口坐标

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 模型/视图分类。