First Commit
This commit is contained in:
commit
3796ec5cd2
146 files changed
+4825
No files matched your search
@@ -0,0 +1,33 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(ClassroomTimer VERSION 1.0 LANGUAGES CXX)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_CXX_STANDARD 14)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
|
||||
if(QT_VERSION_MAJOR EQUAL 5 AND Qt5Core_VERSION VERSION_LESS 5.14.2)
|
||||
message(FATAL_ERROR "Qt 5.14.2 or newer is required.")
|
||||
endif()
|
||||
if(QT_VERSION_MAJOR EQUAL 6)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
endif()
|
||||
if(MSVC)
|
||||
add_compile_options(/utf-8)
|
||||
else()
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
add_executable(ClassroomTimer WIN32 main.cpp appearance.h mainwindow.cpp mainwindow.h countdown.cpp countdown.h mainwindow.ui)
|
||||
target_link_libraries(ClassroomTimer PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
|
||||
option(BUILD_TEACHING_TESTS "Build teaching project tests" OFF)
|
||||
if(BUILD_TEACHING_TESTS)
|
||||
enable_testing()
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
|
||||
add_executable(test_ClassroomTimer tests/test_ClassroomTimer.cpp mainwindow.cpp mainwindow.h
|
||||
countdown.cpp countdown.h mainwindow.ui)
|
||||
target_include_directories(test_ClassroomTimer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(test_ClassroomTimer PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Test)
|
||||
add_test(NAME ClassroomTimer COMMAND test_ClassroomTimer -platform offscreen)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
QT += widgets
|
||||
CONFIG += c++14
|
||||
greaterThan(QT_MAJOR_VERSION, 5): CONFIG += c++17
|
||||
TEMPLATE = app
|
||||
TARGET = ClassroomTimer
|
||||
SOURCES += main.cpp mainwindow.cpp countdown.cpp
|
||||
HEADERS += mainwindow.h countdown.h
|
||||
FORMS += mainwindow.ui
|
||||
msvc: QMAKE_CXXFLAGS += /utf-8
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# 项目二:课堂计时器
|
||||
|
||||
适合第3—4章讲解信号槽、定时器、状态变化和数值输入。可用于限时练习、分组讨论与课堂展示。
|
||||
|
||||
## 功能与操作
|
||||
|
||||
时长范围为 0 到 23:59:59,但零时长不能开始。默认时长为 5 分钟。
|
||||
|
||||
- 开始:从设置的时长开始计时。
|
||||
- 暂停:保存此刻的剩余时间。
|
||||
- 继续:从保存的剩余时间接着计时。
|
||||
- 复位:停止本轮计时,并恢复到设置的完整时长。
|
||||
- 重新开始:到时后开始新的一轮。
|
||||
- 全屏显示:便于投影;按 Esc 退出全屏。
|
||||
- 到时提示:显示“时间到”,并可播放一次系统提示音。能否听到提示音取决于系统声音设置。
|
||||
|
||||
运行中和暂停时,时长输入框均被锁定;先复位,再修改时长。
|
||||
|
||||
## 课堂任务
|
||||
|
||||
先连接开始按钮和 QTimer,让学生观察 timeout 信号重复到达。随后故意安排一次较慢的界面处理,讨论“每次 timeout 减 100 毫秒”为什么可能不准确。
|
||||
|
||||
QTimer 在这个项目中每隔约 100 毫秒请求刷新显示。实际剩余时间由 QElapsedTimer 提供的经过时间计算,不依赖刷新次数。即使某次刷新较晚,下一次仍按实际经过的时间计算。
|
||||
|
||||
讲解模型时可先使用确定的数值:总时长 10 秒,在第 2.5 秒暂停,还剩 7.5 秒;暂停 1 分钟后继续,剩余时间仍从 7.5 秒开始。
|
||||
|
||||
## 核心数据
|
||||
|
||||
Countdown 保存四项信息:完整时长、当前计时片段开始时的剩余量、该片段的开始时刻,以及状态。
|
||||
|
||||
运行时:
|
||||
剩余毫秒 = max(0, 片段开始时的剩余毫秒 − 本片段已过去的毫秒)。
|
||||
|
||||
暂停时先算出剩余毫秒并保存,再切换到 Paused。继续时只记录新的片段开始时刻,不恢复完整时长。
|
||||
|
||||
界面显示秒数采用向上取整。还剩 1 毫秒时显示 1 秒,真正到时才显示 0,避免“已经显示零但还没结束”的现象。
|
||||
|
||||
## 状态与操作
|
||||
|
||||
| 当前状态 | 开始按钮 | 暂停按钮 | 时长输入 |
|
||||
|---|---|---|---|
|
||||
| Ready | 开始,零时长除外 | 不可用 | 可修改 |
|
||||
| Running | 不可用 | 暂停 | 锁定 |
|
||||
| Paused | 继续 | 不可用 | 锁定 |
|
||||
| Finished | 重新开始 | 不可用 | 可修改 |
|
||||
|
||||
refreshDisplay 根据同一个状态统一设置按钮,避免在多个槽函数中分别修改后产生矛盾。
|
||||
|
||||
## 学生练习
|
||||
|
||||
1. 加入 1、3、5 分钟快捷设置按钮,并遵守运行中不能改变时长的规则。
|
||||
2. 最后 10 秒改变数字颜色,到时和复位后恢复正确颜色。
|
||||
3. 增加本轮标题,例如“独立练习”。标题变化不应重新开始计时。
|
||||
|
||||
## 验收用例
|
||||
|
||||
- 设置 1 秒并开始:到时显示 00:00:00,暂停按钮不可用。
|
||||
- 运行时连续触发开始操作:不会重新计算起点。
|
||||
- 暂停后等待:剩余时间保持不变;继续后接着减少。
|
||||
- 运行或暂停时点击复位:回到原设置时长,并允许修改。
|
||||
- 设置零时长:不能开始。
|
||||
- 模型两次更新之间跨过截止时间:剩余量直接归零,不出现负数。
|
||||
|
||||
自动化测试通过传入固定的毫秒数检查计时规则;另有真实按钮测试确认计时模型与界面的连接。
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include <QApplication>
|
||||
#include <QFont>
|
||||
#include <QStyleFactory>
|
||||
|
||||
inline void applyAppearance(QApplication &app)
|
||||
{
|
||||
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
|
||||
app.setFont(QFont(QStringLiteral("Microsoft YaHei UI"), 11));
|
||||
app.setStyleSheet(QStringLiteral(
|
||||
"QMainWindow { background: #f5f7fb; }"
|
||||
"QLabel { color: #23364c; }"
|
||||
"QLabel#titleLabel { font-size: 27px; font-weight: bold; color: #173b63; }"
|
||||
"QLabel#subtitleLabel, QLabel#helpLabel { color: #52657a; }"
|
||||
"QGroupBox { background: white; border: 1px solid #d6e0eb;"
|
||||
" border-radius: 8px; margin-top: 12px; padding: 18px 12px 12px; }"
|
||||
"QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; }"
|
||||
"QLineEdit, QSpinBox, QComboBox { min-height: 32px; padding: 3px 8px; }"
|
||||
"QLineEdit, QSpinBox, QComboBox, QPlainTextEdit, QListWidget"
|
||||
" { background: white; color: #23364c; }"
|
||||
"QPushButton { min-height: 34px; padding: 4px 14px; }"
|
||||
"QPushButton#convertButton, QPushButton#startButton, QPushButton#drawButton,"
|
||||
" QPushButton#applyButton, QPushButton#groupsButton { background: #255c93;"
|
||||
" color: white; border: 1px solid #255c93; border-radius: 5px; }"
|
||||
"QPushButton:disabled, QPushButton#convertButton:disabled,"
|
||||
" QPushButton#startButton:disabled, QPushButton#drawButton:disabled,"
|
||||
" QPushButton#applyButton:disabled, QPushButton#groupsButton:disabled"
|
||||
" { color: #8995a3; background: #e6ebf1; border-color: #d6e0eb; }"
|
||||
"QLabel#statusLabel { color: #185376; }"
|
||||
"QLineEdit#resultEdit { font-size: 23px; min-height: 45px; color: #175e53; }"
|
||||
"QLabel#timeLabel { font: bold 76px 'Consolas'; color: #173b63; }"
|
||||
"QProgressBar { border: 1px solid #d6e0eb; border-radius: 6px; height: 14px; }"
|
||||
"QProgressBar::chunk { background: #2b8a7d; border-radius: 5px; }"));
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "countdown.h"
|
||||
|
||||
Countdown::Countdown(qint64 durationMs)
|
||||
: m_duration(qMax<qint64>(0, durationMs)), m_remainingAtStart(m_duration)
|
||||
{
|
||||
}
|
||||
|
||||
bool Countdown::setDuration(qint64 durationMs)
|
||||
{
|
||||
if (m_state == State::Running || m_state == State::Paused || durationMs < 0)
|
||||
return false;
|
||||
m_duration = durationMs;
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Countdown::start(qint64 nowMs)
|
||||
{
|
||||
if (m_state == State::Running || m_duration <= 0)
|
||||
return;
|
||||
if (m_state != State::Paused)
|
||||
m_remainingAtStart = m_duration;
|
||||
m_segmentStarted = nowMs;
|
||||
m_state = State::Running;
|
||||
}
|
||||
|
||||
qint64 Countdown::remaining(qint64 nowMs) const
|
||||
{
|
||||
if (m_state == State::Finished)
|
||||
return 0;
|
||||
if (m_state != State::Running)
|
||||
return m_remainingAtStart;
|
||||
const qint64 elapsed = qMax<qint64>(0, nowMs - m_segmentStarted);
|
||||
return qMax<qint64>(0, m_remainingAtStart - elapsed);
|
||||
}
|
||||
|
||||
void Countdown::update(qint64 nowMs)
|
||||
{
|
||||
if (m_state == State::Running && remaining(nowMs) == 0) {
|
||||
m_remainingAtStart = 0;
|
||||
m_state = State::Finished;
|
||||
}
|
||||
}
|
||||
|
||||
void Countdown::pause(qint64 nowMs)
|
||||
{
|
||||
update(nowMs);
|
||||
if (m_state != State::Running)
|
||||
return;
|
||||
// 先保存这一刻的剩余时间,再切换状态,暂停期间不继续扣减。
|
||||
m_remainingAtStart = remaining(nowMs);
|
||||
m_state = State::Paused;
|
||||
}
|
||||
|
||||
void Countdown::reset()
|
||||
{
|
||||
m_remainingAtStart = m_duration;
|
||||
m_segmentStarted = 0;
|
||||
m_state = State::Ready;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <QtGlobal>
|
||||
|
||||
// 界面负责取得当前时间,模型只负责计算;测试可传入确定的毫秒数。
|
||||
class Countdown {
|
||||
public:
|
||||
enum class State { Ready, Running, Paused, Finished };
|
||||
explicit Countdown(qint64 durationMs = 300000);
|
||||
bool setDuration(qint64 durationMs);
|
||||
void start(qint64 nowMs);
|
||||
void pause(qint64 nowMs);
|
||||
void reset();
|
||||
void update(qint64 nowMs);
|
||||
qint64 remaining(qint64 nowMs) const;
|
||||
qint64 duration() const { return m_duration; }
|
||||
State state() const { return m_state; }
|
||||
private:
|
||||
qint64 m_duration;
|
||||
qint64 m_remainingAtStart;
|
||||
qint64 m_segmentStarted = 0;
|
||||
State m_state = State::Ready;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "mainwindow.h"
|
||||
#include "appearance.h"
|
||||
#include <QApplication>
|
||||
#include <QFont>
|
||||
#include <QStyleFactory>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||
#endif
|
||||
QApplication app(argc, argv);
|
||||
applyAppearance(app);
|
||||
MainWindow window;
|
||||
window.show();
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QShortcut>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), ui(new Ui::MainWindow)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
m_clock.start();
|
||||
m_refreshTimer.setInterval(100);
|
||||
connect(&m_refreshTimer, &QTimer::timeout, this, &MainWindow::refreshDisplay);
|
||||
for (QSpinBox *spin : {ui->hoursSpin, ui->minutesSpin, ui->secondsSpin})
|
||||
connect(spin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
|
||||
this, &MainWindow::durationChanged);
|
||||
connect(ui->startButton, &QPushButton::clicked, this, [this] {
|
||||
m_countdown.start(m_clock.elapsed());
|
||||
refreshDisplay();
|
||||
});
|
||||
connect(ui->pauseButton, &QPushButton::clicked, this, [this] {
|
||||
const auto previousState = m_countdown.state();
|
||||
m_countdown.pause(m_clock.elapsed());
|
||||
if (previousState == Countdown::State::Running
|
||||
&& m_countdown.state() == Countdown::State::Finished
|
||||
&& ui->soundCheck->isChecked())
|
||||
QApplication::beep();
|
||||
refreshDisplay();
|
||||
});
|
||||
connect(ui->resetButton, &QPushButton::clicked, this, [this] {
|
||||
m_countdown.reset();
|
||||
refreshDisplay();
|
||||
});
|
||||
connect(ui->fullscreenButton, &QPushButton::clicked, this, [this] {
|
||||
if (isFullScreen()) showNormal(); else showFullScreen();
|
||||
ui->fullscreenButton->setText(isFullScreen() ? tr("退出全屏") : tr("全屏显示"));
|
||||
});
|
||||
auto *escape = new QShortcut(QKeySequence(Qt::Key_Escape), this);
|
||||
connect(escape, &QShortcut::activated, this, [this] {
|
||||
if (isFullScreen()) {
|
||||
showNormal();
|
||||
ui->fullscreenButton->setText(tr("全屏显示"));
|
||||
}
|
||||
});
|
||||
durationChanged();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() { delete ui; }
|
||||
|
||||
void MainWindow::durationChanged()
|
||||
{
|
||||
const qint64 seconds = ui->hoursSpin->value() * 3600
|
||||
+ ui->minutesSpin->value() * 60 + ui->secondsSpin->value();
|
||||
m_countdown.setDuration(seconds * 1000);
|
||||
refreshDisplay();
|
||||
}
|
||||
|
||||
void MainWindow::refreshDisplay()
|
||||
{
|
||||
const qint64 now = m_clock.elapsed();
|
||||
const auto previousState = m_countdown.state();
|
||||
m_countdown.update(now);
|
||||
const auto state = m_countdown.state();
|
||||
const bool running = state == Countdown::State::Running;
|
||||
const bool paused = state == Countdown::State::Paused;
|
||||
if (running && !m_refreshTimer.isActive()) m_refreshTimer.start();
|
||||
if (!running) m_refreshTimer.stop();
|
||||
const qint64 remaining = m_countdown.remaining(now);
|
||||
// 向上取整:还剩 1 毫秒时显示 1 秒,真正到时才显示零。
|
||||
const qint64 seconds = (remaining + 999) / 1000;
|
||||
ui->timeLabel->setText(QStringLiteral("%1:%2:%3")
|
||||
.arg(seconds / 3600, 2, 10, QLatin1Char('0'))
|
||||
.arg((seconds / 60) % 60, 2, 10, QLatin1Char('0'))
|
||||
.arg(seconds % 60, 2, 10, QLatin1Char('0')));
|
||||
ui->progressBar->setValue(m_countdown.duration() == 0 ? 0
|
||||
: static_cast<int>(remaining * 1000 / m_countdown.duration()));
|
||||
ui->durationGroup->setEnabled(!running && !paused);
|
||||
ui->startButton->setEnabled(!running && m_countdown.duration() > 0);
|
||||
ui->pauseButton->setEnabled(running);
|
||||
ui->startButton->setText(paused ? tr("继续") :
|
||||
state == Countdown::State::Finished ? tr("重新开始") : tr("开始"));
|
||||
if (running) ui->statusLabel->setText(tr("正在计时"));
|
||||
else if (paused) ui->statusLabel->setText(tr("已暂停 · 点击“继续”接着计时"));
|
||||
else if (state == Countdown::State::Finished)
|
||||
ui->statusLabel->setText(tr("时间到!本轮计时已结束。"));
|
||||
else ui->statusLabel->setText(m_countdown.duration() > 0
|
||||
? tr("准备就绪 · 点击“开始”") : tr("请先设置大于 0 的时长。"));
|
||||
if (previousState == Countdown::State::Running && state == Countdown::State::Finished
|
||||
&& ui->soundCheck->isChecked())
|
||||
QApplication::beep();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include <QMainWindow>
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
#include "countdown.h"
|
||||
namespace Ui { class MainWindow; }
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
private:
|
||||
Ui::MainWindow *ui;
|
||||
Countdown m_countdown;
|
||||
QElapsedTimer m_clock;
|
||||
QTimer m_refreshTimer;
|
||||
void durationChanged();
|
||||
void refreshDisplay();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="windowTitle">
|
||||
<string>课堂计时器</string>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>820</width>
|
||||
<height>650</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="rootLayout">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="titleLabel">
|
||||
<property name="text">
|
||||
<string>课堂计时器</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="subtitleLabel">
|
||||
<property name="text">
|
||||
<string>用于课堂练习、分组讨论与限时展示。</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="durationGroup">
|
||||
<property name="title">
|
||||
<string>设置时长</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="durationLayout">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="hoursSpinLabel">
|
||||
<property name="text">
|
||||
<string>小时</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QSpinBox" name="hoursSpin">
|
||||
<property name="maximum">
|
||||
<number>23</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="minutesSpinLabel">
|
||||
<property name="text">
|
||||
<string>分钟</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="minutesSpin">
|
||||
<property name="maximum">
|
||||
<number>59</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>5</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="secondsSpinLabel">
|
||||
<property name="text">
|
||||
<string>秒</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QSpinBox" name="secondsSpin">
|
||||
<property name="maximum">
|
||||
<number>59</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="timeLabel">
|
||||
<property name="text">
|
||||
<string>00:05:00</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>135</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="statusLabel">
|
||||
<property name="text">
|
||||
<string>准备就绪</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>45</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="timerActions">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="startButton">
|
||||
<property name="text">
|
||||
<string>开始</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pauseButton">
|
||||
<property name="text">
|
||||
<string>暂停</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetButton">
|
||||
<property name="text">
|
||||
<string>复位</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="fullscreenButton">
|
||||
<property name="text">
|
||||
<string>全屏显示</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="soundCheck">
|
||||
<property name="text">
|
||||
<string>到时播放系统提示音</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="helpLabel">
|
||||
<property name="text">
|
||||
<string>计时中和暂停时锁定时长;点击“复位”后可重新设置。全屏时按 Esc 返回。</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources />
|
||||
<connections />
|
||||
</ui>
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <QtTest>
|
||||
#include <QSpinBox>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QCheckBox>
|
||||
#include "countdown.h"
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include "appearance.h"
|
||||
|
||||
static void savePreview(QWidget &window, const QString &fileName)
|
||||
{
|
||||
const QString folder = QString::fromLocal8Bit(qgetenv("QT_TEACHING_SCREENSHOTS"));
|
||||
if (folder.isEmpty()) return;
|
||||
QDir().mkpath(folder);
|
||||
window.show();
|
||||
QApplication::processEvents();
|
||||
QVERIFY(window.grab().save(QDir(folder).filePath(fileName)));
|
||||
}
|
||||
|
||||
class TimerTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase() { applyAppearance(*qApp); }
|
||||
void pauseResumeAndDrift()
|
||||
{
|
||||
Countdown model(10000);
|
||||
model.start(100);
|
||||
model.start(500); // 重复开始不能重新计时。
|
||||
QCOMPARE(model.remaining(2600), qint64(7500));
|
||||
model.pause(2600);
|
||||
QCOMPARE(model.remaining(90000), qint64(7500));
|
||||
QVERIFY(!model.setDuration(2000));
|
||||
model.start(100000);
|
||||
QCOMPARE(model.remaining(102500), qint64(5000));
|
||||
model.update(107500);
|
||||
QVERIFY(model.state() == Countdown::State::Finished);
|
||||
QCOMPARE(model.remaining(107500), qint64(0));
|
||||
model.start(200000);
|
||||
QCOMPARE(model.remaining(200000), qint64(10000));
|
||||
}
|
||||
void delayedRefreshAndReset()
|
||||
{
|
||||
Countdown model(5000);
|
||||
model.start(0);
|
||||
model.update(100);
|
||||
model.update(4300);
|
||||
QCOMPARE(model.remaining(4300), qint64(700));
|
||||
model.update(9999);
|
||||
QCOMPARE(model.remaining(9999), qint64(0));
|
||||
QVERIFY(model.state() == Countdown::State::Finished);
|
||||
model.reset();
|
||||
QVERIFY(model.state() == Countdown::State::Ready);
|
||||
QCOMPARE(model.remaining(9999), qint64(5000));
|
||||
}
|
||||
void zeroAndDeadlinePause()
|
||||
{
|
||||
Countdown model(0);
|
||||
model.start(0);
|
||||
QVERIFY(model.state() == Countdown::State::Ready);
|
||||
QVERIFY(!model.setDuration(-1));
|
||||
QVERIFY(model.setDuration(1000));
|
||||
model.start(10);
|
||||
model.pause(1010);
|
||||
QVERIFY(model.state() == Countdown::State::Finished);
|
||||
}
|
||||
void uiWorkflow()
|
||||
{
|
||||
MainWindow w; w.show();
|
||||
auto *start = w.findChild<QPushButton *>(QStringLiteral("startButton"));
|
||||
auto *pause = w.findChild<QPushButton *>(QStringLiteral("pauseButton"));
|
||||
auto *reset = w.findChild<QPushButton *>(QStringLiteral("resetButton"));
|
||||
auto *minutes = w.findChild<QSpinBox *>(QStringLiteral("minutesSpin"));
|
||||
auto *seconds = w.findChild<QSpinBox *>(QStringLiteral("secondsSpin"));
|
||||
auto *time = w.findChild<QLabel *>(QStringLiteral("timeLabel"));
|
||||
w.findChild<QCheckBox *>(QStringLiteral("soundCheck"))->setChecked(false);
|
||||
savePreview(w, QStringLiteral("02_timer.png"));
|
||||
minutes->setValue(0); seconds->setValue(1);
|
||||
QTest::mouseClick(start, Qt::LeftButton);
|
||||
QVERIFY(!minutes->isEnabled());
|
||||
QTest::mouseClick(pause, Qt::LeftButton);
|
||||
QCOMPARE(start->text(), QStringLiteral("继续"));
|
||||
const QString pausedText = time->text();
|
||||
QTest::qWait(150);
|
||||
QCOMPARE(time->text(), pausedText);
|
||||
QTest::mouseClick(start, Qt::LeftButton);
|
||||
QTRY_COMPARE(time->text(), QStringLiteral("00:00:00"));
|
||||
QVERIFY(w.findChild<QLabel *>(QStringLiteral("statusLabel"))->text().contains(QStringLiteral("时间到")));
|
||||
QVERIFY(!pause->isEnabled());
|
||||
QTest::mouseClick(reset, Qt::LeftButton);
|
||||
QCOMPARE(time->text(), QStringLiteral("00:00:01"));
|
||||
QVERIFY(minutes->isEnabled());
|
||||
seconds->setValue(0);
|
||||
QVERIFY(!start->isEnabled());
|
||||
}
|
||||
};
|
||||
QTEST_MAIN(TimerTest)
|
||||
#include "test_ClassroomTimer.moc"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
QT += widgets testlib
|
||||
CONFIG += c++14 console testcase
|
||||
greaterThan(QT_MAJOR_VERSION, 5): CONFIG += c++17
|
||||
TEMPLATE = app
|
||||
TARGET = test_ClassroomTimer
|
||||
INCLUDEPATH += ..
|
||||
SOURCES += test_ClassroomTimer.cpp ../mainwindow.cpp ../countdown.cpp
|
||||
HEADERS += ../mainwindow.h ../countdown.h
|
||||
FORMS += ../mainwindow.ui
|
||||
msvc: QMAKE_CXXFLAGS += /utf-8
|
||||
|
||||
Reference in new issue
Block a user