在做文件保存提示框需求时,发现很多地方都会判断文件是否保存,未保存时就需要弹框提示,但是操作完又需要根据调用时的情况做后续处理。保存要提示,保存方式覆盖还是另存要提示,另存命名要编辑,命名后有重命名检测,以此等等,所以我叫他任务链,要根据当前任务上下文来做不同的操作。一开始我就在对话框中放一个枚举变量,弹出时设置对应的值,操作结束后根据这个枚举做后续操作。如下:
Connections{
target: warn_save
onAccepted: {
edit_save.show();
}
onRejected: {
switch(type){ //根据枚举值来判断后续操作
case 1: editor.saveFile(arg); break;
case 2: editor.saveAse(arg); break;
case 3: root.close(); break;
}
root.editDoing = false;
}
}
这里存在一个很明显的问题,就是组件的封装性不足,每次增加新的需求就需要扩充枚举,然后修改这个 switch 逻辑,甚至还可能把已有的逻辑改出问题。而且变动一个弹框的接口,可能前后相关的弹框都要跟着变更。
这时我就想到注册回调函数,将变化点转移到调用者,不去变动封装好的组件。即弹框的确认、取消等每一个操作都对应一个回调函数。
//询问是否保存文件
//未保存时退出,或者手动点保存都会弹出
MessageDialog {
id: quit_warn
title: "警告"
text: "文件已编辑,是否保存?(不要点关闭按钮,没处理)"
standardButtons: MessageDialog.Yes | MessageDialog.Cancel
//确认之后需要执行的任务
property var yesTask: null
//取消之后需要执行的任务
property var cancelTask: null
//执行任务
function doing(yes = null, cancel = null)
{
yesTask = yes;
cancelTask = cancel;
open();
}
onYes: {
//选择保存
visible = false;
if(yesTask){
yesTask();
}else{
//默认操作
}
}
onRejected: {
//取消
visible = false;
if(cancelTask){
cancelTask();
}else{
//默认操作
}
}
}
很明显,如果操作链上有多个弹框,那么就成了回调地狱的样子:
//模拟点击退出程序
Button {
text: "退出"
font.family: "SimSun"
font.pixelSize: 16
onClicked: {
if(root.fileEdited){
//未保存就提示保存
quit_warn.doing(function(){
//确认保存就弹保存选择
save_warn.doing(function(){
//覆盖保存
root.close();
},function(){
//另存为
save_name.doing(function(){
//另存确认
root.close();
},function(){
//另存取消
save_warn.visible = true;
});
},function(){
//取消保存,退出
root.close();
});
},function(){
//不保存,退出
root.close();
});
}else{
//未编辑,直接退出
root.close();
}
}
}
对于回调地狱,Web 中的 JS 还可以用 Promise 或者 async / await,但是目前 Qt5 中的 QML 应该还不支持,需要等新的版本。可以先拆分成多个函数,这样比怼在一起更有条理。等想到更好的方案再来补充。
完整测试代码:
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Dialogs 1.2
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Task Chain")
property bool fileEdited: true
function saveFile()
{
console.log('save file');
//假装保存了文件,清除编辑状态
root.fileEdited = false;
}
function saveAs(path)
{
console.log('save as',path);
//假装保存了文件,清除编辑状态
root.fileEdited = false;
}
Column {
anchors.centerIn: parent
spacing: 20
//模拟点击后保存文件
Button {
text: "保存"
font.family: "SimSun"
font.pixelSize: 16
onClicked: {
save_warn.doing(null,null,null);
}
}
//模拟点击退出程序
Button {
text: "退出"
font.family: "SimSun"
font.pixelSize: 16
onClicked: {
if(root.fileEdited){
//未保存就提示保存
quit_warn.doing(function(){
//确认保存就弹保存选择
save_warn.doing(function(){
//覆盖保存
root.close();
},function(){
//另存为
save_name.doing(function(){
//另存确认
root.close();
},function(){
//另存取消
save_warn.visible = true;
});
},function(){
//取消保存,退出
root.close();
});
},function(){
//不保存,退出
root.close();
});
}else{
//未编辑,直接退出
root.close();
}
}
}
}
//询问是否保存文件
//未保存时退出,或者手动点保存都会弹出
MessageDialog {
id: quit_warn
title: "警告"
text: "文件已编辑,是否保存?(不要点关闭按钮,没处理)"
standardButtons: MessageDialog.Yes | MessageDialog.Cancel
//确认之后需要执行的任务
property var yesTask: null
//取消之后需要执行的任务
property var cancelTask: null
//执行任务
function doing(yes = null, cancel = null)
{
yesTask = yes;
cancelTask = cancel;
open();
}
onYes: {
//选择保存
visible = false;
if(yesTask){
yesTask();
}else{
//默认操作
}
}
onRejected: {
//取消
visible = false;
if(cancelTask){
cancelTask();
}else{
//默认操作
}
}
}
//提示覆盖还是另存新文件
MessageDialog {
id: save_warn
title: "文件保存"
text: "覆盖(Reset)还是另存(Save)当前文件?(不要点关闭按钮,没处理)"
standardButtons: MessageDialog.Reset | MessageDialog.Save | MessageDialog.Cancel
//覆盖之后需要执行的任务
property var resetTask: null
//另存之后需要执行的任务
property var saveTask: null
//取消之后需要执行的任务
property var cancelTask: null
//执行任务
function doing(reset = null, save = null, cancel = null)
{
resetTask = reset;
saveTask = save;
cancelTask = cancel;
open();
}
onReset: {
root.saveFile();
//选择覆盖
visible = false;
if(resetTask){
resetTask();
}else{
//默认操作
}
}
onAccepted: {
//选择另存
visible = false;
if(saveTask){
saveTask();
}else{
//默认操作
save_name.doing(null,function(){
save_warn.visible = true;
});
}
}
onRejected: {
//取消
visible = false;
if(cancelTask){
cancelTask();
}else{
//默认操作
}
}
}
//文件命名,后面可能还有重名等提示,这里略去
MessageDialog {
id: save_name
title: "文件命名"
text: "这里懒得做编辑框(不要点关闭按钮,没处理)"
standardButtons: MessageDialog.Yes | MessageDialog.Cancel
//确认之后需要执行的任务
property var yesTask: null
//取消之后需要执行的任务
property var cancelTask: null
//执行任务
function doing(yes = null, cancel = null)
{
yesTask = yes;
cancelTask = cancel;
open();
}
onYes: {
root.saveAs(save_name.text);
//选择保存
visible = false;
if(yesTask){
yesTask();
}else{
//默认操作
}
}
onRejected: {
//取消
visible = false;
if(cancelTask){
cancelTask();
}else{
//默认操作
}
}
}
}