Day4 教学示例补三个:QDialog 综合 / QLineEdit 小眼睛 / 文件 IO(+顶层构建注册)

- qdialog_showcase:wiki「6 对话框 QDialog」6.2–6.5 综合演示。含两套实现:
  · Designer 表单版(dialog.{h,cpp,ui} + resources.qrc + source.gif,QMovie 播 GIF)
  · 纯代码版(qt_dialog_showcase.cpp,单文件,QWidget 布满按钮逐个演示 QDialog 行为)
- lineedit_eye_toggle:子类化 QLineEdit,paintEvent 自绘小眼睛、mouseEvent 命中切换
  echoMode,并与内置 clear button 动态避让(读 objectName=="clearButton" 子 widget 几何)。
  含独立子目录 CMakeLists.txt 供单独打开(顶层 add_subdirectory 不递归)。
- file_io_demo:QFile/QTextStream/QDataStream/QFileInfo/QDir/QTextCodec/QJson 五主题
  综合示例,每个按钮产出一个文件落到 demo_output/ 便于课堂手动查看。

构建注册:examples/CMakeLists.txt 用 add_teaching_example 注册前两个纯代码示例,
mandelbrot 多源文件单独 add_executable;calculator_mainwindow_qss 加 themes.qrc 与
AUTORCC;all_teaching_examples 依赖列表同步补齐。

附 lineedit_eye_toggle 设计文档(docs/superpowers/specs)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
张宗平
2026-07-09 16:51:09 +08:00
parent 993aa30a1c
commit 01b264fe97
16 changed files with 1694 additions and 3 deletions
@@ -0,0 +1,45 @@
cmake_minimum_required(VERSION 3.5)
project(qt_dialog_showcase LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# QtCreator supports the following variables for Android, which are identical to qmake Android variables.
# Check http://doc.qt.io/qt-5/deployment-android.html for more information.
# They need to be set before the find_package(Qt5 ...) call.
#if(ANDROID)
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
# if (ANDROID_ABI STREQUAL "armeabi-v7a")
# set(ANDROID_EXTRA_LIBS
# ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so
# ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so)
# endif()
#endif()
find_package(Qt5 COMPONENTS Widgets REQUIRED)
if(ANDROID)
add_library(qt_dialog_showcase
qt_dialog_showcase.cpp
dialog.cpp
dialog.h
dialog.ui
)
else()
add_executable(qt_dialog_showcase
dialog.cpp
dialog.h
dialog.ui
resources.qrc
qt_dialog_showcase.cpp)
endif()
target_link_libraries(qt_dialog_showcase PRIVATE Qt5::Widgets)
@@ -0,0 +1,78 @@
# QDialog 教学综合示例(qt_dialog_showcase
参考川大 wiki「6 对话框 QDialog」(pageId 183664881) 的 **6.26.5**,形态为一个
**QWidget 布满 pushbutton**:每个按钮触发一种 `QDialog` 行为。纯代码(无 `.ui`),主类不
定义信号槽,全部用 lambda 连接 `QPushButton::clicked`
> 仓库 `p03/ch06` 已有零散单行为示例(modal_dialog / modeless_dialog_* / message_box_* /
> file_dialog);本示例是**综合演示**,把页面的全部行为聚到一个窗口对照体验。
## 16 个按钮 ↔ 页面行为
### ① 模态 / 非模态(6.3)
| 按钮 | 行为 | 关键 API / 要点 |
|---|---|---|
| 模态 exec | `QDialog::exec()` | 应用级模态,阻塞整个应用直到关闭 |
| 窗口模态 open | `QDialog::open()` | 窗口级模态,仅阻塞父窗口 |
| 非模态 show | `new QDialog->show()` | 非模态,立即返回;**栈对象会一闪而过**(6.3 的坑) |
| 非模态+删除关闭 | `setAttribute(WA_DeleteOnClose)+show()` | 关闭即 delete,避免无 parent 堆对象泄漏 |
### ② QMessageBox6.4
| 按钮 | 行为 |
|---|---|
| about / aboutQt / critical / information / question / warning | 六个 static 函数 |
| 自定义消息框 | 实例 API`setIcon / setText / setInformativeText / setDetailedText / setStandardButtons / setDefaultButton` + `exec` |
### ③ 标准对话框(6.2 / 6.5
| 按钮 | 行为 |
|---|---|
| 打开文件 | `QFileDialog::getOpenFileName` |
| 保存文件 | `QFileDialog::getSaveFileName` |
| 选颜色 | `QColorDialog::getColor` |
| 选字体 | `QFontDialog::getFont` |
| 输入文字 | `QInputDialog::getText` |
底部 `QLabel` 显示标准对话框 / question / 自定义消息框的返回值。
## 关键教学点
- **exec vs open vs show**:三种模态/非模态实现,对应 6.3。`exec()` 阻塞整个应用;
`open()` 阻塞父窗口;`show()` 不阻塞。
- **栈对象坑(6.3**`QDialog d; d.show();` 在函数返回时 `d` 析构,对话框立即消失 →
必须用堆对象(`new`)。
- **WA_DeleteOnClose6.3**:无 parent 的堆对象靠它在关闭时自动 `delete`,避免内存泄漏。
- **QMessageBox static vs 实例(6.4**staticabout / question / …)调用最简;实例 API
可设详细文本(`setDetailedText` 出现「显示详情...」按钮)与任意按钮组合。
- **标准对话框统一风格(6.2/6.5)**:`QFileDialog / QColorDialog / QFontDialog / QInputDialog`
都是 `get...` static,返回用户选择,`isEmpty()`/`isValid()`/`&ok` 判断是否取消。
## 目录结构
```
qdialog_showcase/
├── qt_dialog_showcase.cpp # DialogShowcase : QWidget(无 Q_OBJECT,纯 lambda+ main
└── README.md # 本文档
```
## 构建与运行(Windows + MinGW 7.3.0 + Qt 5.14.2
> 必须用与 Qt 5.14.2 ABI 匹配的 mingw730_64,不可用 PATH 中更高版本的 g++。
```bash
# 配置(在仓库根)
cmake -S . -B build_mw -G "MinGW Makefiles" \
-DCMAKE_PREFIX_PATH=E:/Qt/Qt5.14.2/5.14.2/mingw73_64 \
-DCMAKE_CXX_COMPILER=E:/Qt/Qt5.14.2/Tools/mingw730_64/bin/g++.exe \
-DCMAKE_C_COMPILER=E:/Qt/Qt5.14.2/Tools/mingw730_64/bin/gcc.exe
# 构建单目标
cmake --build build_mw --target qt_dialog_showcase
# 运行(GUI,逐个点按钮体验)
PATH=/e/Qt/Qt5.14.2/5.14.2/mingw73_64/bin:$PATH \
QT_QPA_PLATFORM_PLUGIN_PATH=/e/Qt/Qt5.14.2/5.14.2/mingw73_64/plugins/platforms \
./build_mw/docs/teaching/examples/qt_dialog_showcase.exe
# 离屏自动退出验证(不点按钮,退出码应为 0)
QT_QPA_PLATFORM=offscreen \
QT_QPA_PLATFORM_PLUGIN_PATH=/e/Qt/Qt5.14.2/5.14.2/mingw73_64/plugins/platforms \
PATH=/e/Qt/Qt5.14.2/5.14.2/mingw73_64/bin:$PATH \
./build_mw/docs/teaching/examples/qt_dialog_showcase.exe; echo "exit=$?"
```
@@ -0,0 +1,20 @@
#include "dialog.h"
#include "ui_dialog.h"
#include <QMovie>
#include <QDebug>
Dialog::Dialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::Dialog)
{
ui->setupUi(this);
this->pMovie = new QMovie(":/source.gif");
this->pMovie->start();
ui->label->setMovie(this->pMovie);
}
Dialog::~Dialog()
{
delete this->pMovie;
delete ui;
}
@@ -0,0 +1,23 @@
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
namespace Ui {
class Dialog;
}
class Dialog : public QDialog
{
Q_OBJECT
public:
explicit Dialog(QWidget *parent = nullptr);
~Dialog();
private:
Ui::Dialog *ui;
QMovie *pMovie;
};
#endif // DIALOG_H
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>480</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>10</x>
<y>440</y>
<width>621</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QLabel" name="label2">
<property name="geometry">
<rect>
<x>120</x>
<y>70</y>
<width>100</width>
<height>100</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>100</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources.qrc">:/source.gif</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>140</x>
<y>190</y>
<width>200</width>
<height>241</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>200</height>
</size>
</property>
<property name="text">
<string>MarioGif</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -0,0 +1,188 @@
// ============================================================================
// qdialog_showcase —— QDialog 教学综合示例
// 参考川大 wiki「6 对话框 QDialog」(pageId 183664881) 的 6.26.5
// 6.3 模态/非模态(exec / open / show + WA_DeleteOnClose
// 6.4 QMessageBox(六个 static + 实例 API
// 6.2/6.5 标准对话框(QFileDialog / QColorDialog / QFontDialog / QInputDialog
// 形态:一个 QWidget 布满 pushbutton,每个按钮触发一种 QDialog 行为。
// 纯代码(无 .ui);主类不定义信号槽,全部用 lambda 连接 QPushButton::clicked。
// ============================================================================
#include <utility>
#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QGroupBox>
#include <QGridLayout>
#include <QPushButton>
#include <QLabel>
#include <QDialog>
#include <QMessageBox>
#include <QFileDialog>
#include <QColorDialog>
#include <QFontDialog>
#include <QInputDialog>
#include <QLineEdit>
#include <QTimer>
#include "dialog.h"
class DialogShowcase : public QWidget {
public:
explicit DialogShowcase(QWidget *parent = nullptr) : QWidget(parent) {
auto *root = new QVBoxLayout(this);
// 反馈标签:显示标准对话框 / question 的返回值
statusLabel = new QLabel(QStringLiteral("点击下方按钮,体验不同的 QDialog 行为。"));
statusLabel->setWordWrap(true);
statusLabel->setMinimumHeight(36);
// ① 模态/非模态(6.3 -------------------------------------------------
auto *g1 = new QGroupBox(QStringLiteral("① 模态/非模态(6.3"));
auto *l1 = new QGridLayout(g1);
addBtn(l1, 0, 0, QStringLiteral("模态 exec"), [this] {
QDialog d(this);
d.setWindowTitle(QStringLiteral("模态对话框(exec"));
d.exec(); // 应用级模态:阻塞整个应用,直到关闭
setStatus(QStringLiteral("exec() 返回 —— 应用级模态已关闭"));
});
addBtn(l1, 0, 1, QStringLiteral("窗口模态 open"), [this] {
auto *d = new QDialog(this);
d->setWindowTitle(QStringLiteral("窗口级模态(open"));
d->setAttribute(Qt::WA_DeleteOnClose);
d->open(); // 窗口级模态:仅阻塞父窗口,其余窗口仍可交互
setStatus(QStringLiteral("open() 弹出窗口级模态(仅阻塞父窗口)"));
});
addBtn(l1, 1, 0, QStringLiteral("非模态 show"), [this] {
// 必须用堆对象:栈对象在 show() 返回后随作用域结束而析构,对话框一闪而过
// (页面 6.3 强调的坑)。这里给 parent this,由 Qt 对象树托管生命周期。
auto *d = new QDialog(this);
d->setWindowTitle(QStringLiteral("非模态对话框(show"));
d->show(); // 非模态:立即返回、不阻塞,可与主窗口同时操作
setStatus(QStringLiteral("show() 非模态弹出(不阻塞)"));
});
addBtn(l1, 1, 1, QStringLiteral("非模态+删除关闭"), [this] {
// 无 parent 的堆对象:靠 WA_DeleteOnClose 在关闭时自动 delete,避免泄漏
auto *d = new QDialog;
d->setWindowTitle(QStringLiteral("WA_DeleteOnClose"));
d->setAttribute(Qt::WA_DeleteOnClose);
d->show();
setStatus(QStringLiteral("show() + WA_DeleteOnClose:关闭即销毁"));
});
// ② QMessageBox6.4 -------------------------------------------------
auto *g2 = new QGroupBox(QStringLiteral("② QMessageBox6.4"));
auto *l2 = new QGridLayout(g2);
addBtn(l2, 0, 0, QStringLiteral("about"), [this] {
QMessageBox::about(this, QStringLiteral("about"),
QStringLiteral("这是 about 对话框(只有一个 OK)。"));
});
addBtn(l2, 0, 1, QStringLiteral("aboutQt"), [this] {
QMessageBox::aboutQt(this, QStringLiteral("aboutQt"));
});
addBtn(l2, 0, 2, QStringLiteral("critical"), [this] {
QMessageBox::critical(this, QStringLiteral("critical"), QStringLiteral("严重错误!"));
});
addBtn(l2, 1, 0, QStringLiteral("information"), [this] {
QMessageBox::information(this, QStringLiteral("information"), QStringLiteral("提示信息。"));
});
addBtn(l2, 1, 1, QStringLiteral("question"), [this] {
const auto r = QMessageBox::question(this, QStringLiteral("question"),
QStringLiteral("你确定吗?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes);
setStatus(QStringLiteral("question 返回:%1")
.arg(r == QMessageBox::Yes ? QStringLiteral("Yes") : QStringLiteral("No")));
});
addBtn(l2, 1, 2, QStringLiteral("warning"), [this] {
QMessageBox::warning(this, QStringLiteral("warning"), QStringLiteral("警告!"));
});
addBtn(l2, 2, 0, QStringLiteral("自定义消息框"), [this] {
QMessageBox box(QMessageBox::Question, QStringLiteral("自定义消息框"),
QStringLiteral("文档已被修改。"),
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, this);
box.setInformativeText(QStringLiteral("是否保存修改?"));
box.setDetailedText(QStringLiteral("这里是详细差异信息……"));
box.setDefaultButton(QMessageBox::Save);
const int r = box.exec();
const QString name =
(r == QMessageBox::Save) ? QStringLiteral("保存") :
(r == QMessageBox::Discard) ? QStringLiteral("丢弃") :
QStringLiteral("取消");
setStatus(QStringLiteral("自定义消息框返回:%1").arg(name));
});
// ③ 标准对话框(6.2 / 6.5 -------------------------------------------
auto *g3 = new QGroupBox(QStringLiteral("③ 标准对话框(6.2 / 6.5"));
auto *l3 = new QGridLayout(g3);
addBtn(l3, 0, 0, QStringLiteral("打开文件"), [this] {
const QString f = QFileDialog::getOpenFileName(this, QStringLiteral("打开文件"));
if (!f.isEmpty()) setStatus(QStringLiteral("打开文件:%1").arg(f));
});
addBtn(l3, 0, 1, QStringLiteral("保存文件"), [this] {
const QString f = QFileDialog::getSaveFileName(this, QStringLiteral("保存文件"));
if (!f.isEmpty()) setStatus(QStringLiteral("保存文件:%1").arg(f));
});
addBtn(l3, 1, 0, QStringLiteral("选颜色"), [this] {
const QColor c = QColorDialog::getColor(Qt::white, this, QStringLiteral("选颜色"));
if (c.isValid())
setStatus(QStringLiteral("选中颜色 RGB(%1,%2,%3)")
.arg(c.red()).arg(c.green()).arg(c.blue()));
});
addBtn(l3, 1, 1, QStringLiteral("选字体"), [this] {
bool ok = false;
const QFont f = QFontDialog::getFont(&ok, font(), this, QStringLiteral("选字体"));
if (ok) setStatus(QStringLiteral("选中字体:%1").arg(f.family()));
});
addBtn(l3, 2, 0, QStringLiteral("输入文字"), [this] {
bool ok = false;
const QString s = QInputDialog::getText(this, QStringLiteral("输入文字"),
QStringLiteral("请输入:"),
QLineEdit::Normal, QString(), &ok);
if (ok) setStatus(QStringLiteral("输入:%1").arg(s));
});
addBtn(l3, 2, 0, QStringLiteral("输入文字"), [this] {
bool ok = false;
const QString s = QInputDialog::getText(this, QStringLiteral("输入文字"),
QStringLiteral("请输入:"),
QLineEdit::Normal, QString(), &ok);
if (ok) setStatus(QStringLiteral("输入:%1").arg(s));
});
addBtn(l3, 2, 1, QStringLiteral("自定义对话框"), [] {
QDialog *customDialog = new Dialog();
customDialog->setAttribute(Qt::WA_DeleteOnClose);
customDialog->exec();
});
root->addWidget(g1);
root->addWidget(g2);
root->addWidget(g3);
root->addWidget(statusLabel);
setWindowTitle(QStringLiteral("QDialog 教学综合示例"));
resize(520, 430);
}
private:
QLabel *statusLabel = nullptr;
// 在 QGridLayout(row, col) 放一个按钮,clicked 连到 lambda
template <typename F>
void addBtn(QGridLayout *layout, int row, int col, const QString &text, F &&slot) {
auto *btn = new QPushButton(text, this);
layout->addWidget(btn, row, col);
connect(btn, &QPushButton::clicked, this, std::forward<F>(slot));
}
void setStatus(const QString &s) { statusLabel->setText(s); }
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
DialogShowcase w;
w.show();
// 仅在离屏自动化验证时 200ms 后自动退出(仿 p03 风格);
// 正常运行窗口保持,不自动退出。不点按钮则不触发任何对话框。
if (qgetenv("QT_QPA_PLATFORM") == "offscreen") {
QTimer::singleShot(200, &app, &QApplication::quit);
}
return app.exec();
}
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>source.gif</file>
</qresource>
</RCC>
Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB