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(UnitConverter 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(UnitConverter WIN32 main.cpp appearance.h mainwindow.cpp mainwindow.h conversion.cpp conversion.h mainwindow.ui)
|
||||
target_link_libraries(UnitConverter 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_UnitConverter tests/test_UnitConverter.cpp mainwindow.cpp mainwindow.h
|
||||
conversion.cpp conversion.h mainwindow.ui)
|
||||
target_include_directories(test_UnitConverter PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(test_UnitConverter PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Test)
|
||||
add_test(NAME UnitConverter COMMAND test_UnitConverter -platform offscreen)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 项目一:单位换算器
|
||||
|
||||
适合第2—3章讲解信号槽、组件访问和函数分工,第4章可继续练习下拉框与字符串处理。
|
||||
|
||||
## 运行后试什么
|
||||
|
||||
初始输入为 25,源单位为摄氏度,目标单位为华氏度。点击“换算”,应显示 77 °F,并在下方显示公式。也可以在输入框内按 Enter。
|
||||
|
||||
切换到长度后,默认将米换成厘米。输入 1,结果应为 100 cm。“交换单位”只交换源单位与目标单位,输入值保持不变。修改输入或单位会清空旧结果,防止把上次的答案当成当前答案。
|
||||
|
||||
支持摄氏度、华氏度、开尔文,以及毫米、厘米、米、千米、英寸和英尺。结果最多显示 12 位有效数字。
|
||||
|
||||
## 课堂任务
|
||||
|
||||
第一步只保留温度输入、换算按钮和结果框。让学生完成摄氏度到华氏度的换算,并解释输入框返回的是字符串,计算需要数值。
|
||||
|
||||
第二步加入返回值检查。分别输入空字符串、abc 和 25.5,观察程序怎样决定是否继续计算。当前工程用完整字符串的格式检查配合 toDouble,支持小数和科学计数法,不接受千位分隔符。
|
||||
|
||||
第三步加入源单位和目标单位。先统一到摄氏度,再计算目标温度。让学生画出华氏度到开尔文的两步过程,避免只背诵一个总公式。
|
||||
|
||||
第四步加入长度换算。单位比例写在数组中,先解释数组下标与下拉框顺序的对应关系,再讨论增加一个单位时需要修改哪些位置。
|
||||
|
||||
## 一次换算怎样完成
|
||||
|
||||
1. convertButton 发出 clicked 信号,调用 convertValue。
|
||||
2. 主窗口读取 inputEdit 的文本和两个下拉框的索引。
|
||||
3. Conversion::convert 检查输入,计算结果,返回 Result。
|
||||
4. 主窗口根据 Result::ok 显示结果或错误原因。
|
||||
5. 计算成功后允许复制;输入变化后立即禁用复制并清除旧结果。
|
||||
|
||||
Conversion::convert 不直接操作输入框或标签,因此可以使用不同界面调用,也可以单独测试。
|
||||
|
||||
## 需要解释的条件
|
||||
|
||||
- 温度不能低于绝对零度。摄氏度下限为 −273.15,华氏度下限为 −459.67,开尔文下限为 0。
|
||||
- 边界比较保留极小的浮点误差余量,防止换算时的舍入把绝对零度判为非法值。
|
||||
- 本工具中的长度表示非负长度,不表示带方向的坐标位移。
|
||||
- 输入可以合法,但结果仍可能溢出。输入检查和结果检查分别负责两个阶段。
|
||||
- 显示的有效数字有限,程序不承诺任意精度计算。
|
||||
|
||||
## 学生练习
|
||||
|
||||
1. 增加“码”,已知 1 yd = 0.9144 m。检查它与米、英尺之间的换算。
|
||||
2. 增加“使用本次结果继续换算”按钮,说明它与“交换单位”的区别。
|
||||
3. 为结果增加可选择的显示精度,并解释显示精度为什么不等于计算精度。
|
||||
|
||||
## 验收用例
|
||||
|
||||
| 操作 | 预期 |
|
||||
|---|---|
|
||||
| 0 °C → °F | 32 |
|
||||
| 212 °F → °C | 100 |
|
||||
| −459.67 °F → K | 0 |
|
||||
| −274 °C → K | 显示低于绝对零度的提示 |
|
||||
| 1 in → cm | 2.54 |
|
||||
| 1 ft → m | 0.3048 |
|
||||
| 输入 12abc、nan 或 1,000 | 拒绝计算 |
|
||||
| 算出结果后修改输入 | 旧结果清空,复制按钮不可用 |
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
QT += widgets
|
||||
CONFIG += c++14
|
||||
greaterThan(QT_MAJOR_VERSION, 5): CONFIG += c++17
|
||||
TEMPLATE = app
|
||||
TARGET = UnitConverter
|
||||
SOURCES += main.cpp mainwindow.cpp conversion.cpp
|
||||
HEADERS += mainwindow.h conversion.h
|
||||
FORMS += mainwindow.ui
|
||||
msvc: QMAKE_CXXFLAGS += /utf-8
|
||||
|
||||
@@ -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,94 @@
|
||||
#include "conversion.h"
|
||||
#include <QRegularExpression>
|
||||
#include <cmath>
|
||||
|
||||
namespace Conversion {
|
||||
QStringList units(Category category)
|
||||
{
|
||||
if (category == Category::Temperature)
|
||||
return {QStringLiteral("摄氏度 °C"), QStringLiteral("华氏度 °F"),
|
||||
QStringLiteral("开尔文 K")};
|
||||
return {QStringLiteral("毫米 mm"), QStringLiteral("厘米 cm"),
|
||||
QStringLiteral("米 m"), QStringLiteral("千米 km"),
|
||||
QStringLiteral("英寸 in"), QStringLiteral("英尺 ft")};
|
||||
}
|
||||
|
||||
Result convert(const QString &text, Category category, int from, int to)
|
||||
{
|
||||
Result result;
|
||||
const QString input = text.trimmed();
|
||||
// 先检查完整输入,避免把“12abc”误当成有效数值。
|
||||
static const QRegularExpression number(
|
||||
QStringLiteral("^[+-]?(?:[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+)(?:[eE][+-]?[0-9]+)?$"));
|
||||
if (!number.match(input).hasMatch()) {
|
||||
result.error = QStringLiteral("请输入完整的数值,例如 25、-10.5 或 1e3;小数点使用英文句点。");
|
||||
return result;
|
||||
}
|
||||
bool ok = false;
|
||||
const double value = input.toDouble(&ok);
|
||||
if (!ok || !std::isfinite(value)) {
|
||||
result.error = QStringLiteral("输入超出了可计算的数值范围,请减小数值。");
|
||||
return result;
|
||||
}
|
||||
const QStringList names = units(category);
|
||||
if (from < 0 || to < 0 || from >= names.size() || to >= names.size()) {
|
||||
result.error = QStringLiteral("请选择有效的单位。");
|
||||
return result;
|
||||
}
|
||||
if (category == Category::Temperature) {
|
||||
// 先统一为摄氏度,再换算为目标单位,避免为每对单位各写一套公式。
|
||||
double celsius = value;
|
||||
QString first;
|
||||
if (from == 1) {
|
||||
celsius = (value - 32.0) / 1.8;
|
||||
first = QStringLiteral("先换成摄氏度:C = (F − 32) ÷ 1.8。");
|
||||
} else if (from == 2) {
|
||||
celsius = value - 273.15;
|
||||
first = QStringLiteral("先换成摄氏度:C = K − 273.15。");
|
||||
} else {
|
||||
first = QStringLiteral("输入已经是摄氏度,可直接作为中间值。");
|
||||
}
|
||||
if (celsius < -273.15 - 1e-9) {
|
||||
result.error = QStringLiteral("温度不能低于绝对零度:−273.15 °C / −459.67 °F / 0 K。");
|
||||
return result;
|
||||
}
|
||||
// 允许边界处的浮点舍入误差,避免把绝对零度误判为非法输入。
|
||||
if (celsius < -273.15)
|
||||
celsius = -273.15;
|
||||
QString second;
|
||||
if (to == 1) {
|
||||
result.value = celsius * 1.8 + 32.0;
|
||||
second = QStringLiteral("再换成华氏度:F = C × 1.8 + 32。");
|
||||
} else if (to == 2) {
|
||||
result.value = celsius + 273.15;
|
||||
second = QStringLiteral("再换成开尔文:K = C + 273.15。");
|
||||
} else {
|
||||
result.value = celsius;
|
||||
second = QStringLiteral("目标是摄氏度,中间值就是结果。");
|
||||
}
|
||||
result.explanation = first + QStringLiteral("\n") + second;
|
||||
} else {
|
||||
if (value < 0.0) {
|
||||
result.error = QStringLiteral("本工具换算的是长度,输入值不能为负数。");
|
||||
return result;
|
||||
}
|
||||
const double metersPerUnit[] = {0.001, 0.01, 1.0, 1000.0, 0.0254, 0.3048};
|
||||
result.value = value * (metersPerUnit[from] / metersPerUnit[to]);
|
||||
result.explanation =
|
||||
QStringLiteral("以米为统一单位:结果 = 输入值 × 源单位的米数 ÷ 目标单位的米数。\n"
|
||||
"本次计算:%1 × %2 ÷ %3。")
|
||||
.arg(input)
|
||||
.arg(metersPerUnit[from], 0, 'g', 12)
|
||||
.arg(metersPerUnit[to], 0, 'g', 12);
|
||||
}
|
||||
if (!std::isfinite(result.value)) {
|
||||
result.error = QStringLiteral("换算结果超出了可显示的数值范围。");
|
||||
return result;
|
||||
}
|
||||
if (result.value == 0.0)
|
||||
result.value = 0.0; // 避免显示 -0。
|
||||
result.ok = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace Conversion {
|
||||
enum class Category { Temperature, Length };
|
||||
|
||||
struct Result {
|
||||
bool ok = false;
|
||||
double value = 0.0;
|
||||
QString explanation;
|
||||
QString error;
|
||||
};
|
||||
|
||||
QStringList units(Category category);
|
||||
Result convert(const QString &text, Category category, int from, int to);
|
||||
}
|
||||
|
||||
@@ -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,83 @@
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include "conversion.h"
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), ui(new Ui::MainWindow)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->categoryCombo->addItems({tr("温度"), tr("长度")});
|
||||
connect(ui->categoryCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
|
||||
this, &MainWindow::changeCategory);
|
||||
connect(ui->convertButton, &QPushButton::clicked, this, &MainWindow::convertValue);
|
||||
connect(ui->inputEdit, &QLineEdit::returnPressed, this, &MainWindow::convertValue);
|
||||
connect(ui->inputEdit, &QLineEdit::textChanged, this, &MainWindow::clearResult);
|
||||
connect(ui->fromCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
|
||||
this, &MainWindow::clearResult);
|
||||
connect(ui->toCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
|
||||
this, &MainWindow::clearResult);
|
||||
connect(ui->swapButton, &QPushButton::clicked, this, [this] {
|
||||
const int previousFrom = ui->fromCombo->currentIndex();
|
||||
ui->fromCombo->setCurrentIndex(ui->toCombo->currentIndex());
|
||||
ui->toCombo->setCurrentIndex(previousFrom);
|
||||
ui->inputEdit->setFocus();
|
||||
});
|
||||
connect(ui->clearButton, &QPushButton::clicked, this, [this] {
|
||||
ui->inputEdit->clear();
|
||||
clearResult();
|
||||
ui->inputEdit->setFocus();
|
||||
});
|
||||
connect(ui->copyButton, &QPushButton::clicked, this, [this] {
|
||||
QApplication::clipboard()->setText(ui->resultEdit->text());
|
||||
ui->statusLabel->setText(tr("结果已复制。"));
|
||||
});
|
||||
changeCategory();
|
||||
ui->inputEdit->setText(QStringLiteral("25"));
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() { delete ui; }
|
||||
|
||||
void MainWindow::changeCategory()
|
||||
{
|
||||
const auto category = ui->categoryCombo->currentIndex() == 0
|
||||
? Conversion::Category::Temperature : Conversion::Category::Length;
|
||||
const QStringList names = Conversion::units(category);
|
||||
ui->fromCombo->clear();
|
||||
ui->toCombo->clear();
|
||||
ui->fromCombo->addItems(names);
|
||||
ui->toCombo->addItems(names);
|
||||
ui->fromCombo->setCurrentIndex(category == Conversion::Category::Length ? 2 : 0);
|
||||
ui->toCombo->setCurrentIndex(1);
|
||||
clearResult();
|
||||
}
|
||||
|
||||
void MainWindow::clearResult()
|
||||
{
|
||||
ui->resultEdit->clear();
|
||||
ui->formulaLabel->setText(tr("选择单位并输入数值,点击“换算”查看计算过程。"));
|
||||
ui->statusLabel->clear();
|
||||
ui->copyButton->setEnabled(false);
|
||||
}
|
||||
|
||||
void MainWindow::convertValue()
|
||||
{
|
||||
const auto category = ui->categoryCombo->currentIndex() == 0
|
||||
? Conversion::Category::Temperature : Conversion::Category::Length;
|
||||
const auto result = Conversion::convert(ui->inputEdit->text(), category,
|
||||
ui->fromCombo->currentIndex(),
|
||||
ui->toCombo->currentIndex());
|
||||
if (!result.ok) {
|
||||
clearResult();
|
||||
ui->statusLabel->setText(result.error);
|
||||
ui->inputEdit->setFocus();
|
||||
return;
|
||||
}
|
||||
ui->resultEdit->setText(QStringLiteral("%1 %2")
|
||||
.arg(result.value, 0, 'g', 12).arg(ui->toCombo->currentText()));
|
||||
ui->formulaLabel->setText(result.explanation);
|
||||
ui->statusLabel->setText(tr("换算完成。结果最多显示 12 位有效数字。"));
|
||||
ui->copyButton->setEnabled(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <QMainWindow>
|
||||
namespace Ui { class MainWindow; }
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
private:
|
||||
Ui::MainWindow *ui;
|
||||
void changeCategory();
|
||||
void clearResult();
|
||||
void convertValue();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<?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>840</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="inputGroup">
|
||||
<property name="title">
|
||||
<string>输入与单位</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="inputLayout" columnstretch="1,1">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="categoryLabel">
|
||||
<property name="text">
|
||||
<string>换算类型</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="inputLabel">
|
||||
<property name="text">
|
||||
<string>输入数值</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QComboBox" name="categoryCombo" />
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="inputEdit">
|
||||
<property name="placeholderText">
|
||||
<string>例如 25、-10.5 或 1e3</string>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="fromLabel">
|
||||
<property name="text">
|
||||
<string>源单位</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="toLabel">
|
||||
<property name="text">
|
||||
<string>目标单位</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QComboBox" name="fromCombo" />
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="toCombo" />
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="actionLayout">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="convertButton">
|
||||
<property name="text">
|
||||
<string>换算</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="swapButton">
|
||||
<property name="text">
|
||||
<string>交换单位</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="clearButton">
|
||||
<property name="text">
|
||||
<string>清空输入</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="resultGroup">
|
||||
<property name="title">
|
||||
<string>换算结果</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="resultLayout">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="resultEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>结果将在这里显示</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="formulaLabel">
|
||||
<property name="text">
|
||||
<string />
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>70</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="copyButton">
|
||||
<property name="text">
|
||||
<string>复制结果</string>
|
||||
</property>
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="statusLabel">
|
||||
<property name="text">
|
||||
<string />
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>55</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="helpLabel">
|
||||
<property name="text">
|
||||
<string>长度须为非负数,温度须不低于绝对零度。修改输入或单位后,需要重新换算。</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,90 @@
|
||||
#include <QtTest>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include "conversion.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 ConverterTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase() { applyAppearance(*qApp); }
|
||||
void knownValues_data()
|
||||
{
|
||||
QTest::addColumn<int>("category");
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<int>("from");
|
||||
QTest::addColumn<int>("to");
|
||||
QTest::addColumn<double>("expected");
|
||||
QTest::newRow("freezing") << 0 << QStringLiteral("0") << 0 << 1 << 32.0;
|
||||
QTest::newRow("boiling") << 0 << QStringLiteral("212") << 1 << 0 << 100.0;
|
||||
QTest::newRow("minus40") << 0 << QStringLiteral("-40") << 0 << 1 << -40.0;
|
||||
QTest::newRow("kelvin") << 0 << QStringLiteral("273.15") << 2 << 0 << 0.0;
|
||||
QTest::newRow("absolute-zero-C") << 0 << QStringLiteral("-273.15") << 0 << 2 << 0.0;
|
||||
QTest::newRow("absolute-zero-F") << 0 << QStringLiteral("-459.67") << 1 << 2 << 0.0;
|
||||
QTest::newRow("meter-cm") << 1 << QStringLiteral("1") << 2 << 1 << 100.0;
|
||||
QTest::newRow("inch-cm") << 1 << QStringLiteral("1") << 4 << 1 << 2.54;
|
||||
QTest::newRow("foot-meter") << 1 << QStringLiteral("1") << 5 << 2 << 0.3048;
|
||||
QTest::newRow("km-mm") << 1 << QStringLiteral("1") << 3 << 0 << 1000000.0;
|
||||
QTest::newRow("scientific") << 1 << QStringLiteral(" 1e3 ") << 0 << 2 << 1.0;
|
||||
QTest::newRow("same-unit") << 1 << QStringLiteral("12.25") << 2 << 2 << 12.25;
|
||||
}
|
||||
void knownValues()
|
||||
{
|
||||
QFETCH(int, category); QFETCH(QString, input);
|
||||
QFETCH(int, from); QFETCH(int, to); QFETCH(double, expected);
|
||||
auto result = Conversion::convert(input, static_cast<Conversion::Category>(category), from, to);
|
||||
QVERIFY2(result.ok, qPrintable(result.error));
|
||||
QVERIFY(qAbs(result.value - expected) <= 1e-9 * qMax(1.0, qAbs(expected)));
|
||||
}
|
||||
void rejectsInvalid()
|
||||
{
|
||||
for (const QString &text : {QString(), QStringLiteral("12abc"), QStringLiteral("nan"),
|
||||
QStringLiteral("inf"), QStringLiteral("1,000"), QStringLiteral("1e309")})
|
||||
QVERIFY(!Conversion::convert(text, Conversion::Category::Length, 2, 1).ok);
|
||||
QVERIFY(!Conversion::convert(QStringLiteral("-1"), Conversion::Category::Length, 2, 1).ok);
|
||||
QVERIFY(!Conversion::convert(QStringLiteral("-274"), Conversion::Category::Temperature, 0, 2).ok);
|
||||
QVERIFY(!Conversion::convert(QStringLiteral("-0.1"), Conversion::Category::Temperature, 2, 0).ok);
|
||||
QVERIFY(!Conversion::convert(QStringLiteral("1e308"), Conversion::Category::Length, 3, 0).ok);
|
||||
}
|
||||
void uiWorkflow()
|
||||
{
|
||||
MainWindow w; w.show();
|
||||
auto *input = w.findChild<QLineEdit *>(QStringLiteral("inputEdit"));
|
||||
auto *result = w.findChild<QLineEdit *>(QStringLiteral("resultEdit"));
|
||||
auto *convert = w.findChild<QPushButton *>(QStringLiteral("convertButton"));
|
||||
input->setText(QStringLiteral("25"));
|
||||
QTest::mouseClick(convert, Qt::LeftButton);
|
||||
QVERIFY(result->text().startsWith(QStringLiteral("77 ")));
|
||||
savePreview(w, QStringLiteral("01_converter.png"));
|
||||
input->setText(QStringLiteral("bad"));
|
||||
QVERIFY(result->text().isEmpty());
|
||||
QTest::mouseClick(convert, Qt::LeftButton);
|
||||
QVERIFY(!w.findChild<QLabel *>(QStringLiteral("statusLabel"))->text().isEmpty());
|
||||
QVERIFY(!w.findChild<QPushButton *>(QStringLiteral("copyButton"))->isEnabled());
|
||||
w.findChild<QComboBox *>(QStringLiteral("categoryCombo"))->setCurrentIndex(1);
|
||||
input->setText(QStringLiteral("1"));
|
||||
QTest::mouseClick(convert, Qt::LeftButton);
|
||||
QVERIFY(result->text().startsWith(QStringLiteral("100 ")));
|
||||
QTest::mouseClick(w.findChild<QPushButton *>(QStringLiteral("swapButton")), Qt::LeftButton);
|
||||
QTest::mouseClick(convert, Qt::LeftButton);
|
||||
QVERIFY(result->text().startsWith(QStringLiteral("0.01 ")));
|
||||
}
|
||||
};
|
||||
QTEST_MAIN(ConverterTest)
|
||||
#include "test_UnitConverter.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_UnitConverter
|
||||
INCLUDEPATH += ..
|
||||
SOURCES += test_UnitConverter.cpp ../mainwindow.cpp ../conversion.cpp
|
||||
HEADERS += ../mainwindow.h ../conversion.h
|
||||
FORMS += ../mainwindow.ui
|
||||
msvc: QMAKE_CXXFLAGS += /utf-8
|
||||
|
||||
Reference in new issue
Block a user