From e6e5bd32b5647bf8398e57e78eb82299e3ec623f Mon Sep 17 00:00:00 2001 From: liyp Date: Tue, 25 Aug 2026 22:12:29 -0400 Subject: [PATCH] first commit --- .gitignore | 19 + README.md | 182 +++++++ VoiletCStudio.pro | 30 ++ installer/install.sh | 92 ++++ installer/uninstall.sh | 25 + installer/voiletcstudio.nsi | 85 +++ resources.qrc | 6 + resources/voiletcstudio.desktop | 11 + resources/voiletcstudio.ico | Bin 0 -> 270398 bytes resources/voiletcstudio.png | Bin 0 -> 34347 bytes resources/voiletcstudio.svg | 58 ++ run.sh | 3 + src/cmakegenerator.cpp | 316 +++++++++++ src/cmakegenerator.h | 39 ++ src/main.cpp | 177 ++++++ src/mainwindow.cpp | 922 ++++++++++++++++++++++++++++++++ src/mainwindow.h | 105 ++++ src/projectconfig.cpp | 351 ++++++++++++ src/projectconfig.h | 101 ++++ 需求规格说明书.md | 542 +++++++++++++++++++ 20 files changed, 3064 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 VoiletCStudio.pro create mode 100644 installer/install.sh create mode 100644 installer/uninstall.sh create mode 100644 installer/voiletcstudio.nsi create mode 100644 resources.qrc create mode 100644 resources/voiletcstudio.desktop create mode 100644 resources/voiletcstudio.ico create mode 100644 resources/voiletcstudio.png create mode 100644 resources/voiletcstudio.svg create mode 100755 run.sh create mode 100644 src/cmakegenerator.cpp create mode 100644 src/cmakegenerator.h create mode 100644 src/main.cpp create mode 100644 src/mainwindow.cpp create mode 100644 src/mainwindow.h create mode 100644 src/projectconfig.cpp create mode 100644 src/projectconfig.h create mode 100644 需求规格说明书.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..063baec --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Build artifacts +*.o +moc_*.cpp +moc_predefs.h +qrc_*.cpp +Makefile +VoiletCStudio +.qmake.stash + +# IDE +.vscode/ +.idea/ +*.user +*.pro.user + +# Installer output +*.exe +VoiletCStudio-Setup-*.exe +installer/build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c75561f --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# VoiletCStudio - 紫罗兰 C 工具箱 + +Qt5 跨平台 C 工程配置器,自动生成 CMakeLists.txt + +> 版本:v1.1 · 2026-04-28 + +--- + +## 🚀 功能特性 + +### 1. 工程配置管理 +- ✅ JSON 格式配置文件 +- ✅ 新建/打开/保存工程 +- ✅ 拖拽 JSON 文件打开 +- ✅ Ctrl+S 快速保存 + +### 2. 编译工具链配置 +- ✅ 编译器路径选择 +- ✅ 汇编器路径选择 +- ✅ 链接器路径选择 +- ✅ 支持 MinGW / GCC / Clang +- ✅ Windows 下自动识别 MinGW 工具链 + +### 3. 文件管理 +- ✅ 虚拟目录(类似 MDK) +- ✅ .c 源文件分类管理 +- ✅ .h 包含目录管理 +- ✅ 库文件管理(.a/.so/.lib) + +### 4. 编译配置 +- ✅ 编译宏定义 +- ✅ 自定义编译选项 +- ✅ Debug / Release 模式 + +### 5. CMake 生成 +- ✅ 自动生成 CMakeLists.txt +- ✅ 编译器路径在 `project()` 之前设置,避免默认回退到 NMake +- ✅ Windows 下自动指定 `-G "MinGW Makefiles"` 生成器 +- ✅ 内置一键编译(Debug / Release) + +--- + +## 📋 使用方法 + +### 编译 VoiletCStudio + +**Linux:** +```bash +cd VoiletCStudio +qmake +make +``` + +**Windows (MSYS2/MinGW):** +```bash +cd VoiletCStudio +qmake +mingw32-make +``` + +### 运行 +```bash +./VoiletCStudio # Linux +VoiletCStudio.exe # Windows +``` + +### 使用流程 +1. 新建工程 → 输入工程名 +2. 配置编译器路径(Windows 选 `gcc.exe`) +3. 添加虚拟目录和 .c 源文件 +4. 添加包含目录和库文件 +5. 添加编译宏和选项 +6. 保存工程(JSON) +7. 点击「生成 CMake」→ 自动生成 CMakeLists.txt +8. 点击「编译 Debug / Release」→ 一键编译 + +--- + +## 🛠️ CMake 使用 + +```bash +# Linux / macOS +cmake -B build +make debug # 编译 Debug +make release # 编译 Release + +# Windows (MinGW) +cmake -G "MinGW Makefiles" -B build +mingw32-make debug +mingw32-make release +``` + +--- + +## 📁 项目结构 + +``` +VoiletCStudio/ +├── src/ +│ ├── main.cpp # 主程序入口 +│ ├── mainwindow.cpp/h # 主窗口(含编译流程) +│ ├── projectconfig.cpp/h # 配置管理 +│ └── cmakegenerator.cpp/h # CMake 生成 +├── VoiletCStudio.pro # Qt 项目文件 +├── README.md # 说明文档 +└── 需求规格说明书.md # 详细需求文档 +``` + +--- + +## 💻 跨平台支持 + +| 平台 | 编译器 | 生成器 | 状态 | +|------|--------|--------|------| +| Windows | MinGW (MSYS2) | MinGW Makefiles | ✅ | +| Windows | MSVC | NMake | ⚠️ 未测试 | +| Linux | GCC | Unix Makefiles | ✅ | +| macOS | Clang | Unix Makefiles | ✅ | + +### Windows 特别注意 +- 必须安装 **MSYS2** 并安装 `mingw-w64-gcc`、`mingw-w64-cmake` +- 编译器路径示例:`C:/msys2/mingw64/bin/gcc.exe` +- CMake 生成时程序自动使用 `-G "MinGW Makefiles"` +- 构建时使用 `mingw32-make` 而非 `make` + +--- + +## 📝 配置文件格式 + +```json +{ + "projectName": "MyProject", + "projectPath": "/path/to/project", + "outputDir": "./build", + "compilerPath": "/usr/bin/gcc", + "assemblerPath": "/usr/bin/gcc", + "linkerPath": "/usr/bin/gcc", + "virtualDirs": { + "App": { + "name": "App", + "files": ["src/main.c"] + } + }, + "includeDirs": ["./include"], + "libraries": ["libmylib.a"], + "defines": ["DEBUG"], + "compilerOptions": ["-Wall"] +} +``` + +--- + +## 🔧 技术要点 + +### 编译器设置顺序 +CMakeLists.txt 中 `CMAKE_C_COMPILER` **必须在 `project()` 之前设置**,否则 CMake 会在 `project()` 时自动检测编译器并可能回退到不存在的 NMake。 + +### Windows 生成器 +程序通过 `#ifdef Q_OS_WIN32` 编译期检测平台,在 Windows 上自动: +- CMake 添加 `-G "MinGW Makefiles"` 参数 +- 构建时使用 `mingw32-make` 替代 `make` +- 自动从编译器路径推导 `CMAKE_MAKE_PROGRAM` + +--- + +## 🎯 特色 + +- **类 MDK 虚拟目录**:像 Keil MDK 一样管理源文件 +- **一键生成 CMake**:自动生成完整的 CMakeLists.txt +- **真正跨平台**:Windows (MinGW) / Linux / macOS 全支持,自动化平台适配 +- **轻量级**:纯 Qt5 实现,无额外依赖 + +--- + +## 📄 许可证 + +MIT License + +--- + +**作者:虾哥** +**日期:2026-04-09 · 更新:2026-04-28** diff --git a/VoiletCStudio.pro b/VoiletCStudio.pro new file mode 100644 index 0000000..a0ffc2e --- /dev/null +++ b/VoiletCStudio.pro @@ -0,0 +1,30 @@ +QT += core gui widgets + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++11 + +TARGET = VoiletCStudio +TEMPLATE = app + +# 源文件 +SOURCES += \ + src/main.cpp \ + src/mainwindow.cpp \ + src/projectconfig.cpp \ + src/cmakegenerator.cpp + +# 头文件 +HEADERS += \ + src/mainwindow.h \ + src/projectconfig.h \ + src/cmakegenerator.h + +# 资源文件 +RESOURCES += \ + resources.qrc + +# 默认部署规则 +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/installer/install.sh b/installer/install.sh new file mode 100644 index 0000000..e626e5b --- /dev/null +++ b/installer/install.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# ===================================================== +# VoiletCStudio Linux 安装脚本 +# 用法: sudo bash installer/install.sh +# ===================================================== + +set -e + +APP_NAME="VoiletCStudio" +VERSION="1.2" +INSTALL_DIR="/opt/${APP_NAME}" +BIN_DIR="/usr/local/bin" +DESKTOP_DIR="/usr/share/applications" +MIME_DIR="/usr/share/mime/packages" +ICON_DIR="/usr/share/icons/hicolor/256x256/apps" + +echo "=========================================" +echo " VoiletCStudio v${VERSION} - Linux 安装程序" +echo "=========================================" + +# 检查 root +if [ "$EUID" -ne 0 ]; then + echo "请使用 sudo 运行: sudo bash installer/install.sh" + exit 1 +fi + +echo "" +echo "[1/5] 编译程序..." +mkdir -p build && cd build +qmake ../VoiletCStudio.pro && make -j$(nproc) +cd .. + +echo "" +echo "[2/5] 安装到 ${INSTALL_DIR}..." +mkdir -p "${INSTALL_DIR}" +cp build/VoiletCStudio "${INSTALL_DIR}/" +chmod +x "${INSTALL_DIR}/VoiletCStudio" + +# 创建命令行软链接 +ln -sf "${INSTALL_DIR}/VoiletCStudio" "${BIN_DIR}/voiletcstudio" + +echo "" +echo "[3/5] 安装图标..." +mkdir -p "${ICON_DIR}" +cp resources/voiletcstudio.png "${ICON_DIR}/voiletcstudio.png" + +echo "" +echo "[4/5] 注册 MIME 类型和 .desktop..." +mkdir -p "${MIME_DIR}" +cat > "${MIME_DIR}/voiletcstudio.xml" << 'MIMEXML' + + + + VoiletCStudio Project + VoiletCStudio 工程文件 + + + + +MIMEXML + +mkdir -p "${DESKTOP_DIR}" +cat > "${DESKTOP_DIR}/voiletcstudio.desktop" << DESKTOPEOF +[Desktop Entry] +Type=Application +Name=VoiletCStudio +Name[zh_CN]=紫罗兰 C 工程配置器 +Comment=C Project Configurator with CMake +Comment[zh_CN]=C 语言工程配置器,自动生成 CMakeLists.txt +Exec=${INSTALL_DIR}/VoiletCStudio %f +Icon=voiletcstudio +MimeType=application/x-voiletcstudio; +Categories=Development;IDE; +Terminal=false +DESKTOPEOF + +echo "" +echo "[5/5] 更新系统数据库..." +update-mime-database /usr/share/mime +update-desktop-database +gtk-update-icon-cache /usr/share/icons/hicolor 2>/dev/null || true + +echo "" +echo "=========================================" +echo " ✅ VoiletCStudio 安装完成!" +echo "=========================================" +echo "" +echo " 命令行: voiletcstudio" +echo " 或双击 .VSC 工程文件自动打开" +echo "" +echo " 卸载: sudo bash installer/uninstall.sh" +echo "" diff --git a/installer/uninstall.sh b/installer/uninstall.sh new file mode 100644 index 0000000..438342e --- /dev/null +++ b/installer/uninstall.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# ===================================================== +# VoiletCStudio Linux 卸载脚本 +# ===================================================== +set -e + +if [ "$EUID" -ne 0 ]; then + echo "请使用 sudo 运行" + exit 1 +fi + +APP_NAME="VoiletCStudio" + +echo "正在卸载 ${APP_NAME}..." + +rm -rf "/opt/${APP_NAME}" +rm -f "/usr/local/bin/voiletcstudio" +rm -f "/usr/share/applications/voiletcstudio.desktop" +rm -f "/usr/share/mime/packages/voiletcstudio.xml" +rm -f "/usr/share/icons/hicolor/256x256/apps/voiletcstudio.png" + +update-mime-database /usr/share/mime 2>/dev/null || true +update-desktop-database 2>/dev/null || true + +echo "✅ ${APP_NAME} 已卸载" diff --git a/installer/voiletcstudio.nsi b/installer/voiletcstudio.nsi new file mode 100644 index 0000000..922ecf6 --- /dev/null +++ b/installer/voiletcstudio.nsi @@ -0,0 +1,85 @@ +; ===================================================== +; VoiletCStudio Windows 安装脚本 (NSIS) +; 用法: makensis installer/voiletcstudio.nsi +; ===================================================== + +!define PRODUCT_NAME "VoiletCStudio" +!define PRODUCT_VERSION "1.2" +!define PRODUCT_PUBLISHER "LinuxAcme" +!define PRODUCT_EXE "VoiletCStudio.exe" + +Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" +OutFile "VoiletCStudio-Setup-${PRODUCT_VERSION}.exe" +InstallDir "$PROGRAMFILES\${PRODUCT_NAME}" +RequestExecutionLevel admin + +; ----- 安装页面 ----- +Page directory +Page instfiles + +; ----- 默认安装路径 ----- +Section "Install" + SetOutPath "$INSTDIR" + + ; 复制主程序 + File "VoiletCStudio.exe" + + ; 复制 Qt 运行时 DLL(需要提前准备好) + ; File /r "Qt5Core.dll" + ; File /r "Qt5Gui.dll" + ; File /r "Qt5Widgets.dll" + + ; 复制 MinGW 运行时 + ; File /r "libgcc_s_seh-1.dll" + ; File /r "libstdc++-6.dll" + ; File /r "libwinpthread-1.dll" + + ; 创建卸载程序 + WriteUninstaller "$INSTDIR\uninstall.exe" + + ; 注册 .VSC 文件关联 + WriteRegStr HKCR ".VSC" "" "VoiletCStudio.VSC" + WriteRegStr HKCR "VoiletCStudio.VSC" "" "VoiletCStudio 工程文件" + WriteRegStr HKCR "VoiletCStudio.VSC\DefaultIcon" "" "$INSTDIR\${PRODUCT_EXE},0" + WriteRegStr HKCR "VoiletCStudio.VSC\shell\open\command" "" '"$INSTDIR\${PRODUCT_EXE}" "%1"' + + ; 注册表卸载信息 + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" "$INSTDIR\uninstall.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "${PRODUCT_PUBLISHER}" + + ; 开始菜单快捷方式 + CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\VoiletCStudio.lnk" "$INSTDIR\${PRODUCT_EXE}" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\卸载.lnk" "$INSTDIR\uninstall.exe" + + ; 桌面快捷方式 + CreateShortCut "$DESKTOP\VoiletCStudio.lnk" "$INSTDIR\${PRODUCT_EXE}" + + ; 通知系统刷新文件关联 + System::Call 'shell32.dll::SHChangeNotify(i, i, i, i) v (0x08000000, 0, 0, 0)' +SectionEnd + +; ----- 卸载 ----- +Section "Uninstall" + ; 删除程序 + Delete "$INSTDIR\${PRODUCT_EXE}" + Delete "$INSTDIR\uninstall.exe" + RMDir "$INSTDIR" + + ; 删除文件关联 + DeleteRegKey HKCR ".VSC" + DeleteRegKey HKCR "VoiletCStudio.VSC" + + ; 删除注册表 + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" + + ; 删除快捷方式 + Delete "$SMPROGRAMS\${PRODUCT_NAME}\VoiletCStudio.lnk" + Delete "$SMPROGRAMS\${PRODUCT_NAME}\卸载.lnk" + RMDir "$SMPROGRAMS\${PRODUCT_NAME}" + Delete "$DESKTOP\VoiletCStudio.lnk" + + System::Call 'shell32.dll::SHChangeNotify(i, i, i, i) v (0x08000000, 0, 0, 0)' +SectionEnd diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..c79246c --- /dev/null +++ b/resources.qrc @@ -0,0 +1,6 @@ + + + resources/voiletcstudio.png + resources/voiletcstudio.ico + + diff --git a/resources/voiletcstudio.desktop b/resources/voiletcstudio.desktop new file mode 100644 index 0000000..f0e987a --- /dev/null +++ b/resources/voiletcstudio.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=VoiletCStudio +Name[zh_CN]=紫罗兰 C 工程配置器 +Comment=C Project Configurator with CMake +Comment[zh_CN]=C 语言工程配置器,自动生成 CMakeLists.txt +Exec=VoiletCStudio %f +Icon=voiletcstudio +MimeType=application/x-voiletcstudio; +Categories=Development;IDE; +Terminal=false diff --git a/resources/voiletcstudio.ico b/resources/voiletcstudio.ico new file mode 100644 index 0000000000000000000000000000000000000000..70de32d269ed57c13c15dcb115c4eaa40fec4ba0 GIT binary patch literal 270398 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..efd3364 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(dirname "$0")" +./VoiletCStudio diff --git a/src/cmakegenerator.cpp b/src/cmakegenerator.cpp new file mode 100644 index 0000000..7e7bd6e --- /dev/null +++ b/src/cmakegenerator.cpp @@ -0,0 +1,316 @@ +#include "cmakegenerator.h" +#include +#include +#include +#include +#include + +CMakeGenerator::CMakeGenerator(QObject *parent) + : QObject(parent) +{ +} + +bool CMakeGenerator::generate(const ProjectConfig *config, const QString &outputPath) +{ + if (!config) { + emit errorOccurred("配置对象为空"); + return false; + } + + QString content = generateContent(config); + + QFileInfo fileInfo(outputPath); + QDir dir = fileInfo.absoluteDir(); + if (!dir.exists()) { + dir.mkpath("."); + } + + QFile file(outputPath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + emit errorOccurred("无法创建 CMakeLists.txt: " + file.errorString()); + return false; + } + + QTextStream out(&file); + out.setCodec("UTF-8"); + out << content; + file.close(); + + m_lastGeneratedPath = outputPath; + emit cmakeGenerated(outputPath); + + return true; +} + +QString CMakeGenerator::getLastGeneratedPath() const +{ + return m_lastGeneratedPath; +} + +QString CMakeGenerator::generateContent(const ProjectConfig *config) +{ + QString content; + + content += generateHeader(config); + content += "\n"; + content += generateProjectConfig(config); + content += "\n"; + content += generateSourceFiles(config); + content += "\n"; + content += generateIncludeDirs(config); + content += "\n"; + content += generateLibraries(config); + content += "\n"; + content += generateDefines(config); + content += "\n"; + content += generateCompilerOptions(config); + content += "\n"; + content += generateBuildTargets(config); + + return content; +} + +QString CMakeGenerator::generateHeader(const ProjectConfig *config) +{ + QString header; + header += "# ========================================\n"; + header += "# CMakeLists.txt - VoiletCStudio 自动生成\n"; + header += "# 工程名称:" + config->getProjectName() + "\n"; + header += "# 生成时间:" + QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss") + "\n"; + header += "# 跨平台支持:Windows (MinGW) / Linux (GCC) / macOS (Clang)\n"; + header += "# ========================================\n\n"; + + header += "cmake_minimum_required(VERSION 3.10)\n\n"; + + return header; +} + +QString CMakeGenerator::generateProjectConfig(const ProjectConfig *config) +{ + QString content; + + QString projectName = config->getProjectName(); + if (projectName.isEmpty()) { + projectName = "UntitledProject"; + } + + // ===== 编译器必须在 project() 之前设置,否则 CMake 会默认用 NMake ===== + content += "# ========================================\n"; + content += "# 编译器配置(必须在 project() 之前)\n"; + content += "# ========================================\n\n"; + + QString compilerPath = config->getCompilerPath(); + QString assemblerPath = config->getAssemblerPath(); + QString linkerPath = config->getLinkerPath(); + + if (!compilerPath.isEmpty() && compilerPath != "gcc") { + content += "set(CMAKE_C_COMPILER \"" + compilerPath + "\")\n"; + + // 如果编译器是 MinGW 路径,自动推导 make 程序目录 + // 例如:C:/msys2/mingw64/bin/gcc.exe → C:/msys2/mingw64/bin/mingw32-make.exe + if (compilerPath.contains("mingw", Qt::CaseInsensitive)) { + QString makeDir = QFileInfo(compilerPath).path(); // path() 正确保留绝对路径的盘符 + content += "set(CMAKE_MAKE_PROGRAM \"" + makeDir + "/mingw32-make.exe\")\n"; + } + } + + if (!assemblerPath.isEmpty() && assemblerPath != "gcc") { + content += "set(CMAKE_ASM_COMPILER \"" + assemblerPath + "\")\n"; + } + + if (!linkerPath.isEmpty() && linkerPath != "gcc" && linkerPath != compilerPath) { + content += "set(CMAKE_C_LINK_EXECUTABLE \"" + linkerPath + " -o \")\n"; + } + content += "\n"; + + content += "# 项目名称(编译器已设置,不再尝试检测)\n"; + content += "project(" + projectName + " C)\n\n"; + + content += "# C 标准\n"; + content += "set(CMAKE_C_STANDARD 11)\n"; + content += "set(CMAKE_C_STANDARD_REQUIRED ON)\n\n"; + + // 输出目录配置 + QString outputDir = config->getOutputDir(); + if (outputDir.isEmpty()) { + outputDir = "./build"; + } + + content += "# 输出目录配置\n"; + content += "set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/" + outputDir + ")\n"; + content += "set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/" + outputDir + ")\n"; + content += "set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/" + outputDir + ")\n\n"; + + return content; +} + +QString CMakeGenerator::generateSourceFiles(const ProjectConfig *config) +{ + QString content; + content += "# ========================================\n"; + content += "# 源文件配置(虚拟目录结构)\n"; + content += "# ========================================\n\n"; + + QMap virtualDirs = config->getVirtualDirs(); + + if (virtualDirs.isEmpty()) { + content += "# 无源文件\n"; + } else { + // 遍历虚拟目录 + for (auto it = virtualDirs.begin(); it != virtualDirs.end(); ++it) { + content += "# 分组:" + it.key() + "\n"; + QString safeName = it.key(); + safeName.replace(" ", "_"); + safeName.replace("/", "_"); + content += "set(SRCS_" + safeName + "\n"; + for (const QString &file : it->files) { + content += " " + file + "\n"; + } + content += ")\n\n"; + } + + // 合并所有源文件 + content += "# 合并所有源文件\n"; + content += "set(SOURCES\n"; + for (auto it = virtualDirs.begin(); it != virtualDirs.end(); ++it) { + for (const QString &file : it->files) { + content += " " + file + "\n"; + } + } + content += ")\n\n"; + } + + content += "# 创建可执行文件\n"; + QString outputName = config->getProjectName(); + if (outputName.isEmpty()) { + outputName = "output"; + } + + content += "add_executable(" + outputName + " ${SOURCES})\n\n"; + + return content; +} + +QString CMakeGenerator::generateIncludeDirs(const ProjectConfig *config) +{ + QString content; + content += "# 包含目录\n"; + + QStringList includeDirs = config->getIncludeDirs(); + if (includeDirs.isEmpty()) { + content += "# 无额外包含目录\n"; + } else { + content += "target_include_directories(" + config->getProjectName() + " PRIVATE\n"; + for (const QString &dir : includeDirs) { + content += " " + dir + "\n"; + } + content += ")\n"; + } + + return content; +} + +QString CMakeGenerator::generateLibraries(const ProjectConfig *config) +{ + QString content; + content += "# 链接库\n"; + + QStringList libraries = config->getLibraries(); + if (libraries.isEmpty()) { + content += "# 无外部库文件\n"; + } else { + content += "target_link_libraries(" + config->getProjectName() + " PRIVATE\n"; + for (const QString &lib : libraries) { + content += " " + lib + "\n"; + } + content += ")\n"; + } + + return content; +} + +QString CMakeGenerator::generateDefines(const ProjectConfig *config) +{ + QString content; + content += "# 预编译宏定义\n"; + + QStringList defines = config->getDefines(); + if (defines.isEmpty()) { + content += "# 无额外宏定义\n"; + } else { + content += "target_compile_definitions(" + config->getProjectName() + " PRIVATE\n"; + for (const QString &def : defines) { + content += " " + def + "\n"; + } + content += ")\n"; + } + + return content; +} + +QString CMakeGenerator::generateCompilerOptions(const ProjectConfig *config) +{ + QString content; + content += "# 自定义编译选项\n"; + + QStringList options = config->getCompilerOptions(); + if (options.isEmpty()) { + content += "# 无自定义编译选项\n"; + } else { + content += "target_compile_options(" + config->getProjectName() + " PRIVATE\n"; + for (const QString &opt : options) { + content += " " + opt + "\n"; + } + content += ")\n"; + } + + return content; +} + +QString CMakeGenerator::generateBuildTargets(const ProjectConfig * /*config*/) +{ + QString content; + + content += "# ========================================\n"; + content += "# 构建类型配置\n"; + content += "# ========================================\n\n"; + + content += "# 默认构建类型:Debug\n"; + content += "if(NOT CMAKE_BUILD_TYPE)\n"; + content += " set(CMAKE_BUILD_TYPE Debug CACHE STRING \"Build type\" FORCE)\n"; + content += "endif()\n\n"; + + content += "# Debug 模式配置(自动添加 DEBUG 宏)\n"; + content += "set(CMAKE_C_FLAGS_DEBUG \"-g -O0 -DDEBUG\" CACHE STRING \"Debug flags\" FORCE)\n\n"; + + content += "# Release 模式配置\n"; + content += "set(CMAKE_C_FLAGS_RELEASE \"-O2 -DNDEBUG\" CACHE STRING \"Release flags\" FORCE)\n\n"; + + content += "# ========================================\n"; + content += "# 自定义 Make 目标:make debug / make release\n"; + content += "# ========================================\n\n"; + + content += "# make debug - 编译 Debug 版本(自动添加 DEBUG 宏)\n"; + content += "add_custom_target(debug\n"; + content += " COMMAND ${CMAKE_COMMAND} -DCMAKE_BUILD_TYPE=Debug ${CMAKE_SOURCE_DIR}\n"; + content += " COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --config Debug\n"; + content += " COMMENT \"Building Debug version\"\n"; + content += " VERBATIM\n"; + content += ")\n\n"; + + content += "# make release - 编译 Release 版本\n"; + content += "add_custom_target(release\n"; + content += " COMMAND ${CMAKE_COMMAND} -DCMAKE_BUILD_TYPE=Release ${CMAKE_SOURCE_DIR}\n"; + content += " COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --config Release\n"; + content += " COMMENT \"Building Release version\"\n"; + content += " VERBATIM\n"; + content += ")\n\n"; + + content += "# 使用说明:\n"; + content += "# cmake -B build # 配置(默认 Debug)\n"; + content += "# cmake --build build # 编译(使用当前 CMAKE_BUILD_TYPE)\n"; + content += "# make debug # 编译 Debug 版本(自动添加 DEBUG 宏)\n"; + content += "# make release # 编译 Release 版本\n"; + + return content; +} diff --git a/src/cmakegenerator.h b/src/cmakegenerator.h new file mode 100644 index 0000000..e56ec81 --- /dev/null +++ b/src/cmakegenerator.h @@ -0,0 +1,39 @@ +#ifndef CMAKEGENERATOR_H +#define CMAKEGENERATOR_H + +#include +#include +#include "projectconfig.h" + +class CMakeGenerator : public QObject +{ + Q_OBJECT + +public: + explicit CMakeGenerator(QObject *parent = nullptr); + + // 生成 CMakeLists.txt + bool generate(const ProjectConfig *config, const QString &outputPath); + + // 获取最后生成的路径 + QString getLastGeneratedPath() const; + +signals: + void cmakeGenerated(const QString &path); + void errorOccurred(const QString &error); + +private: + QString generateContent(const ProjectConfig *config); + QString generateHeader(const ProjectConfig *config); + QString generateProjectConfig(const ProjectConfig *config); + QString generateSourceFiles(const ProjectConfig *config); + QString generateIncludeDirs(const ProjectConfig *config); + QString generateLibraries(const ProjectConfig *config); + QString generateDefines(const ProjectConfig *config); + QString generateCompilerOptions(const ProjectConfig *config); + QString generateBuildTargets(const ProjectConfig *config); + + QString m_lastGeneratedPath; +}; + +#endif // CMAKEGENERATOR_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bd99db9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "mainwindow.h" + +#ifdef Q_OS_WIN32 +#include +#include +#endif + +// ===== 文件关联注册 ===== +static void registerFileAssociation() +{ +#ifdef Q_OS_WIN32 + QString appPath = QCoreApplication::applicationFilePath(); + appPath.replace("/", "\\"); + + // 注册 .VSC 扩展名 + QSettings settings("HKEY_CLASSES_ROOT\\.VSC", QSettings::NativeFormat); + settings.setValue("Default", "VoiletCStudio.VSC"); + settings.sync(); + + // 注册文件类型描述 + QSettings typeSettings("HKEY_CLASSES_ROOT\\VoiletCStudio.VSC", QSettings::NativeFormat); + typeSettings.setValue("Default", "VoiletCStudio 工程文件"); + typeSettings.sync(); + + // 注册图标 + QSettings iconSettings("HKEY_CLASSES_ROOT\\VoiletCStudio.VSC\\DefaultIcon", QSettings::NativeFormat); + iconSettings.setValue("Default", appPath + ",0"); + iconSettings.sync(); + + // 注册打开命令 + QSettings cmdSettings("HKEY_CLASSES_ROOT\\VoiletCStudio.VSC\\shell\\open\\command", QSettings::NativeFormat); + cmdSettings.setValue("Default", "\"" + appPath + "\" \"%1\""); + cmdSettings.sync(); + + // 通知系统刷新 + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); + + qDebug() << "[OK] .VSC file association registered on Windows"; +#else + // Linux: 写入 MIME 类型定义 + 更新 desktop 数据库 + QString mimeDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/mime/packages"; + QDir().mkpath(mimeDir); + + QString mimeFile = mimeDir + "/voiletcstudio.xml"; + QFile f(mimeFile); + if (f.open(QIODevice::WriteOnly)) { + f.write(R"( + + + VoiletCStudio Project + VoiletCStudio 工程文件 + + + + +)"); + f.close(); + } + + // 写入 .desktop 文件 + QString appsDir = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); + QDir().mkpath(appsDir); + + QString desktopFile = appsDir + "/voiletcstudio.desktop"; + QFile df(desktopFile); + if (df.open(QIODevice::WriteOnly)) { + df.write(R"([Desktop Entry] +Type=Application +Name=VoiletCStudio +Name[zh_CN]=紫罗兰 C 工程配置器 +Comment=C Project Configurator +Comment[zh_CN]=C 语言工程配置器 +Exec=)" + QCoreApplication::applicationFilePath().toUtf8() + R"( %f +Icon=voiletcstudio +MimeType=application/x-voiletcstudio; +Categories=Development;IDE; +Terminal=false +)"); + df.close(); + } + + // 更新 MIME 数据库 + QProcess::execute("update-mime-database", QStringList() << + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/mime"); + QProcess::execute("update-desktop-database", QStringList()); + + qDebug() << "[OK] .VSC file association registered on Linux"; +#endif +} + +static void unregisterFileAssociation() +{ +#ifdef Q_OS_WIN32 + QSettings settings("HKEY_CLASSES_ROOT\\.VSC", QSettings::NativeFormat); + settings.remove("Default"); + settings.sync(); + + QSettings("HKEY_CLASSES_ROOT\\VoiletCStudio.VSC", QSettings::NativeFormat).remove(""); + SHDeleteKey(HKEY_CLASSES_ROOT, L"VoiletCStudio.VSC"); + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); + + qDebug() << "[OK] .VSC file association removed"; +#else + QString mimeFile = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/mime/packages/voiletcstudio.xml"; + QFile::remove(mimeFile); + QProcess::execute("update-mime-database", QStringList() << + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/mime"); + qDebug() << "[OK] .VSC file association removed"; +#endif +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + // 设置应用图标 + app.setWindowIcon(QIcon(":/resources/voiletcstudio.png")); + + // 设置应用程序信息 + app.setApplicationName("VoiletCStudio"); + app.setApplicationVersion("1.2"); + app.setOrganizationName("LinuxAcme"); + + // 命令行参数解析 + QCommandLineParser parser; + parser.setApplicationDescription("紫罗兰 C 工程配置器 - 自动生成 CMakeLists.txt"); + parser.addHelpOption(); + parser.addVersionOption(); + + QCommandLineOption registerOpt("register", "注册 .VSC 文件关联到系统"); + QCommandLineOption unregisterOpt("unregister", "取消 .VSC 文件关联"); + parser.addOption(registerOpt); + parser.addOption(unregisterOpt); + + // 接受 .VSC 文件作为位置参数(双击打开) + parser.addPositionalArgument("file", "要打开的 .VSC 工程文件"); + + parser.process(app); + + // 处理注册/取消注册 + if (parser.isSet(registerOpt)) { + registerFileAssociation(); + return 0; + } + if (parser.isSet(unregisterOpt)) { + unregisterFileAssociation(); + return 0; + } + + // 设置样式 + app.setStyle(QStyleFactory::create("Fusion")); + + MainWindow window; + window.show(); + + // 通过命令行传入的文件自动打开 + QStringList posArgs = parser.positionalArguments(); + if (!posArgs.isEmpty()) { + QString filePath = posArgs.first(); + if (filePath.endsWith(".VSC", Qt::CaseInsensitive) || filePath.endsWith(".json", Qt::CaseInsensitive)) { + window.openProjectFile(filePath); + } + } + + return app.exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..11dd2f9 --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,922 @@ +#include "mainwindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , modified(false) + , showCMakeSuccessMsg(true) +{ + config = new ProjectConfig(this); + cmakeGenerator = new CMakeGenerator(this); + + setupUI(); + setupConnections(); + updateWindowTitle(); +} + +MainWindow::~MainWindow() +{ +} + +void MainWindow::setupUI() +{ + setWindowTitle("VoiletCStudio - C 工程配置器"); + resize(1400, 900); + + // 创建操作按钮栏 + QGroupBox *actionGroup = new QGroupBox("⚡ 快捷操作"); + QHBoxLayout *actionLayout = new QHBoxLayout(actionGroup); + + QPushButton *openBtn = new QPushButton("📂 打开工程"); + openBtn->setMinimumHeight(35); + openBtn->setStyleSheet("QPushButton { background: #1565C0; color: white; font-weight: bold; border: none; border-radius: 4px; padding: 6px 16px; }"); + connect(openBtn, &QPushButton::clicked, this, &MainWindow::openProject); + actionLayout->addWidget(openBtn); + + QPushButton *newBtn = new QPushButton("📄 新建工程"); + newBtn->setMinimumHeight(35); + newBtn->setStyleSheet("QPushButton { background: #2E7D32; color: white; font-weight: bold; border: none; border-radius: 4px; padding: 6px 16px; }"); + connect(newBtn, &QPushButton::clicked, this, &MainWindow::newProject); + actionLayout->addWidget(newBtn); + + saveButton = new QPushButton("💾 保存工程 (Ctrl+S)"); + saveButton->setMinimumHeight(35); + saveButton->setStyleSheet("QPushButton { background: #E65100; color: white; font-weight: bold; border: none; border-radius: 4px; padding: 6px 16px; }"); + connect(saveButton, &QPushButton::clicked, this, &MainWindow::saveProject); + actionLayout->addWidget(saveButton); + + generateButton = new QPushButton("🔨 生成 CMake (Ctrl+G)"); + generateButton->setMinimumHeight(35); + generateButton->setStyleSheet("QPushButton { background: #6A1B9A; color: white; font-weight: bold; border: none; border-radius: 4px; padding: 6px 16px; }"); + connect(generateButton, &QPushButton::clicked, this, &MainWindow::generateCMake); + actionLayout->addWidget(generateButton); + + QPushButton *debugBtn = new QPushButton("🐛 编译 Debug"); + debugBtn->setMinimumHeight(35); + debugBtn->setStyleSheet("QPushButton { background: #00838F; color: white; font-weight: bold; border: none; border-radius: 4px; padding: 6px 16px; }"); + connect(debugBtn, &QPushButton::clicked, this, &MainWindow::compileDebug); + actionLayout->addWidget(debugBtn); + this->debugBtn = debugBtn; + + QPushButton *releaseBtn = new QPushButton("🚀 编译 Release"); + releaseBtn->setMinimumHeight(35); + releaseBtn->setStyleSheet("QPushButton { background: #AD1457; color: white; font-weight: bold; border: none; border-radius: 4px; padding: 6px 16px; }"); + connect(releaseBtn, &QPushButton::clicked, this, &MainWindow::compileRelease); + actionLayout->addWidget(releaseBtn); + this->releaseBtn = releaseBtn; + + actionLayout->addStretch(); + + // 创建中心部件 + QWidget *central = new QWidget(this); + setCentralWidget(central); + + QVBoxLayout *mainLayout = new QVBoxLayout(central); + mainLayout->addWidget(actionGroup); + + // 工程基本信息 + QGroupBox *basicGroup = new QGroupBox("📁 工程基本信息"); + QHBoxLayout *basicLayout = new QHBoxLayout(basicGroup); + + basicLayout->addWidget(new QLabel("📁 工程名:")); + projectNameEdit = new QLineEdit(); + projectNameEdit->setPlaceholderText("输入工程名称"); + basicLayout->addWidget(projectNameEdit); + + basicLayout->addStretch(); + basicLayout->addWidget(new QLabel("📂 工程路径:同 VSC 工程文件所在目录")); + + mainLayout->addWidget(basicGroup); + + // 编译工具链 + QGroupBox *toolchainGroup = new QGroupBox("🔧 编译工具链"); + QHBoxLayout *toolchainLayout = new QHBoxLayout(toolchainGroup); + + toolchainLayout->addWidget(new QLabel("编译器:")); + compilerPathEdit = new QLineEdit(); + compilerPathEdit->setPlaceholderText("/usr/bin/gcc"); + toolchainLayout->addWidget(compilerPathEdit); + + QPushButton *compilerBtn = new QPushButton("浏览"); + connect(compilerBtn, &QPushButton::clicked, this, &MainWindow::browseCompilerPath); + toolchainLayout->addWidget(compilerBtn); + + toolchainLayout->addWidget(new QLabel("汇编器:")); + assemblerPathEdit = new QLineEdit(); + assemblerPathEdit->setPlaceholderText("/usr/bin/gcc"); + toolchainLayout->addWidget(assemblerPathEdit); + + QPushButton *assemblerBtn = new QPushButton("浏览"); + connect(assemblerBtn, &QPushButton::clicked, this, &MainWindow::browseAssemblerPath); + toolchainLayout->addWidget(assemblerBtn); + + toolchainLayout->addWidget(new QLabel("链接器:")); + linkerPathEdit = new QLineEdit(); + linkerPathEdit->setPlaceholderText("/usr/bin/gcc"); + toolchainLayout->addWidget(linkerPathEdit); + + QPushButton *linkerBtn = new QPushButton("浏览"); + connect(linkerBtn, &QPushButton::clicked, this, &MainWindow::browseLinkerPath); + toolchainLayout->addWidget(linkerBtn); + + mainLayout->addWidget(toolchainGroup); + + // 主分割器 - 左右布局 + QSplitter *mainSplitter = new QSplitter(Qt::Horizontal); + + // 左侧 - 文件管理 + QWidget *leftPanel = new QWidget(); + QVBoxLayout *leftLayout = new QVBoxLayout(leftPanel); + leftLayout->setContentsMargins(0, 0, 0, 0); + + // 源文件(虚拟目录) + QGroupBox *sourceGroup = new QGroupBox("📄 源文件 (.c) - 虚拟目录"); + QVBoxLayout *sourceLayout = new QVBoxLayout(sourceGroup); + + QHBoxLayout *sourceBtnLayout = new QHBoxLayout(); + QPushButton *addDirBtn = new QPushButton("➕ 目录"); + connect(addDirBtn, &QPushButton::clicked, this, &MainWindow::addVirtualDir); + sourceBtnLayout->addWidget(addDirBtn); + + QPushButton *addFileBtn = new QPushButton("➕ 文件"); + connect(addFileBtn, &QPushButton::clicked, this, &MainWindow::addSourceFile); + sourceBtnLayout->addWidget(addFileBtn); + + QPushButton *removeFileBtn = new QPushButton("➖ 移除"); + connect(removeFileBtn, &QPushButton::clicked, this, &MainWindow::removeSourceFile); + sourceBtnLayout->addWidget(removeFileBtn); + sourceBtnLayout->addStretch(); + + sourceLayout->addLayout(sourceBtnLayout); + + sourceTree = new QTreeWidget(); + sourceTree->setHeaderLabels(QStringList() << "📁 源文件 (虚拟目录)"); + sourceTree->setSelectionMode(QAbstractItemView::ExtendedSelection); + sourceLayout->addWidget(sourceTree); + + leftLayout->addWidget(sourceGroup); + + // 包含目录 + QGroupBox *includeGroup = new QGroupBox("📚 包含目录 (.h)"); + QVBoxLayout *includeLayout = new QVBoxLayout(includeGroup); + + QHBoxLayout *includeBtnLayout = new QHBoxLayout(); + QPushButton *addIncludeBtn = new QPushButton("➕ 添加"); + connect(addIncludeBtn, &QPushButton::clicked, this, &MainWindow::addIncludeDir); + includeBtnLayout->addWidget(addIncludeBtn); + + QPushButton *removeIncludeBtn = new QPushButton("➖ 移除"); + connect(removeIncludeBtn, &QPushButton::clicked, this, &MainWindow::removeIncludeDir); + includeBtnLayout->addWidget(removeIncludeBtn); + includeBtnLayout->addStretch(); + + includeLayout->addLayout(includeBtnLayout); + + includeDirList = new QListWidget(); + includeLayout->addWidget(includeDirList); + + leftLayout->addWidget(includeGroup); + + mainSplitter->addWidget(leftPanel); + + // 右侧 - 编译配置 + QWidget *rightPanel = new QWidget(); + QVBoxLayout *rightLayout = new QVBoxLayout(rightPanel); + rightLayout->setContentsMargins(0, 0, 0, 0); + + // 库文件 + QGroupBox *libGroup = new QGroupBox("📦 库文件 (.a/.so/.lib)"); + QVBoxLayout *libLayout = new QVBoxLayout(libGroup); + + QHBoxLayout *libBtnLayout = new QHBoxLayout(); + QPushButton *addLibBtn = new QPushButton("➕ 添加"); + connect(addLibBtn, &QPushButton::clicked, this, &MainWindow::addLibrary); + libBtnLayout->addWidget(addLibBtn); + + QPushButton *removeLibBtn = new QPushButton("➖ 移除"); + connect(removeLibBtn, &QPushButton::clicked, this, &MainWindow::removeLibrary); + libBtnLayout->addWidget(removeLibBtn); + libBtnLayout->addStretch(); + + libLayout->addLayout(libBtnLayout); + + libraryList = new QListWidget(); + libLayout->addWidget(libraryList); + + rightLayout->addWidget(libGroup); + + // 编译宏 + QGroupBox *defineGroup = new QGroupBox("🏷️ 编译宏定义"); + QVBoxLayout *defineLayout = new QVBoxLayout(defineGroup); + + QHBoxLayout *defineBtnLayout = new QHBoxLayout(); + QPushButton *addDefineBtn = new QPushButton("➕ 添加"); + connect(addDefineBtn, &QPushButton::clicked, this, &MainWindow::addDefine); + defineBtnLayout->addWidget(addDefineBtn); + + QPushButton *removeDefineBtn = new QPushButton("➖ 移除"); + connect(removeDefineBtn, &QPushButton::clicked, this, &MainWindow::removeDefine); + defineBtnLayout->addWidget(removeDefineBtn); + defineBtnLayout->addStretch(); + + defineLayout->addLayout(defineBtnLayout); + + defineList = new QListWidget(); + defineLayout->addWidget(defineList); + + rightLayout->addWidget(defineGroup); + + // 编译选项 + QGroupBox *optionGroup = new QGroupBox("⚙️ 自定义编译选项"); + QVBoxLayout *optionLayout = new QVBoxLayout(optionGroup); + + QHBoxLayout *optionBtnLayout = new QHBoxLayout(); + QPushButton *addOptionBtn = new QPushButton("➕ 添加"); + connect(addOptionBtn, &QPushButton::clicked, this, &MainWindow::addCompilerOption); + optionBtnLayout->addWidget(addOptionBtn); + + QPushButton *removeOptionBtn = new QPushButton("➖ 移除"); + connect(removeOptionBtn, &QPushButton::clicked, this, &MainWindow::removeCompilerOption); + optionBtnLayout->addWidget(removeOptionBtn); + optionBtnLayout->addStretch(); + + optionLayout->addLayout(optionBtnLayout); + + optionList = new QListWidget(); + optionLayout->addWidget(optionList); + + rightLayout->addWidget(optionGroup); + + mainSplitter->addWidget(rightPanel); + mainSplitter->setStretchFactor(0, 1); + mainSplitter->setStretchFactor(1, 1); + + mainLayout->addWidget(mainSplitter); + + // 状态栏 + statusBar()->showMessage("就绪"); +} + +void MainWindow::setupConnections() +{ + connect(projectNameEdit, &QLineEdit::textChanged, this, &MainWindow::onProjectNameChanged); + connect(compilerPathEdit, &QLineEdit::textChanged, this, &MainWindow::onCompilerPathChanged); + connect(assemblerPathEdit, &QLineEdit::textChanged, this, &MainWindow::onAssemblerPathChanged); + connect(linkerPathEdit, &QLineEdit::textChanged, this, &MainWindow::onLinkerPathChanged); + + connect(config, &ProjectConfig::configChanged, [this]() { + modified = true; + updateWindowTitle(); + }); + + connect(cmakeGenerator, &CMakeGenerator::cmakeGenerated, [this](const QString &path) { + if (showCMakeSuccessMsg) { + statusBar()->showMessage("CMake 生成成功:" + path, 3000); + QMessageBox::information(this, "✅ CMake 生成成功", + "CMakeLists.txt 已生成!\n\n" + "📁 位置:" + path + "\n\n" + "🔨 编译方式:\n" + " cd build\n" + " make debug # 编译 Debug 版本\n" + " make release # 编译 Release 版本"); + } + showCMakeSuccessMsg = true; // 重置为 true,下次生成时还会弹窗 + }); + + connect(cmakeGenerator, &CMakeGenerator::errorOccurred, [this](const QString &error) { + statusBar()->showMessage("错误:" + error, 5000); + QMessageBox::critical(this, "错误", error); + }); +} + +void MainWindow::updateWindowTitle() +{ + QString title = "VoiletCStudio - " + config->getProjectName(); + if (modified) { + title += " [*]"; + } + if (!currentFilePath.isEmpty()) { + title += " - " + currentFilePath; + } + setWindowTitle(title); + setWindowModified(modified); +} + +bool MainWindow::maybeSave() +{ + if (!modified) { + return true; + } + + QMessageBox::StandardButton ret = QMessageBox::warning(this, "VoiletCStudio", + "工程已修改,是否保存?", + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + + if (ret == QMessageBox::Save) { + return saveProject(); + } else if (ret == QMessageBox::Cancel) { + return false; + } + return true; +} + +void MainWindow::newProject() +{ + if (!maybeSave()) { + return; + } + + config = new ProjectConfig(this); + currentFilePath.clear(); + modified = false; + + projectNameEdit->clear(); + compilerPathEdit->clear(); + assemblerPathEdit->clear(); + linkerPathEdit->clear(); + sourceTree->clear(); + includeDirList->clear(); + libraryList->clear(); + defineList->clear(); + optionList->clear(); + + updateWindowTitle(); + statusBar()->showMessage("已创建新工程", 3000); +} + +void MainWindow::openProject() +{ + if (!maybeSave()) { + return; + } + + QString filePath = QFileDialog::getOpenFileName(this, "打开工程", "", + "VSC 工程文件 (*.VSC);;所有文件 (*)"); + + if (filePath.isEmpty()) { + return; + } + + if (config->loadConfig(filePath)) { + currentFilePath = filePath; + modified = false; + + projectNameEdit->setText(config->getProjectName()); + compilerPathEdit->setText(config->getCompilerPath()); + assemblerPathEdit->setText(config->getAssemblerPath()); + linkerPathEdit->setText(config->getLinkerPath()); + + sourceTree->clear(); + QMap dirs = config->getVirtualDirs(); + for (auto it = dirs.begin(); it != dirs.end(); ++it) { + QTreeWidgetItem *dirItem = new QTreeWidgetItem(sourceTree); + dirItem->setText(0, it.key()); + for (const QString &file : it->files) { + QTreeWidgetItem *fileItem = new QTreeWidgetItem(dirItem); + fileItem->setText(0, file); + } + } + + includeDirList->clear(); + includeDirList->addItems(config->getIncludeDirs()); + + libraryList->clear(); + libraryList->addItems(config->getLibraries()); + + defineList->clear(); + defineList->addItems(config->getDefines()); + + optionList->clear(); + optionList->addItems(config->getCompilerOptions()); + + updateWindowTitle(); + statusBar()->showMessage("工程已加载:" + filePath, 3000); + } else { + QMessageBox::critical(this, "错误", "无法加载工程文件:" + filePath); + } +} + +void MainWindow::openProjectFile(const QString &filePath) +{ + // 直接打开指定文件(用于命令行和双击打开),跳过 maybeSave + if (!QFile::exists(filePath)) { + QMessageBox::critical(this, "错误", "文件不存在:" + filePath); + return; + } + + if (config->loadConfig(filePath)) { + currentFilePath = filePath; + modified = false; + + projectNameEdit->setText(config->getProjectName()); + compilerPathEdit->setText(config->getCompilerPath()); + assemblerPathEdit->setText(config->getAssemblerPath()); + linkerPathEdit->setText(config->getLinkerPath()); + + sourceTree->clear(); + QMap dirs = config->getVirtualDirs(); + for (auto it = dirs.begin(); it != dirs.end(); ++it) { + QTreeWidgetItem *dirItem = new QTreeWidgetItem(sourceTree); + dirItem->setText(0, it.key()); + for (const QString &file : it->files) { + QTreeWidgetItem *fileItem = new QTreeWidgetItem(dirItem); + fileItem->setText(0, file); + } + } + sourceTree->expandAll(); + + includeDirList->clear(); + includeDirList->addItems(config->getIncludeDirs()); + + libraryList->clear(); + libraryList->addItems(config->getLibraries()); + + defineList->clear(); + defineList->addItems(config->getDefines()); + + optionList->clear(); + optionList->addItems(config->getCompilerOptions()); + + updateWindowTitle(); + statusBar()->showMessage("✅ 工程已加载:" + filePath, 5000); + } else { + QMessageBox::critical(this, "错误", "无法加载工程文件:" + filePath); + } +} + +bool MainWindow::saveProject() +{ + if (currentFilePath.isEmpty()) { + return saveProjectAs(); + } + + config->setProjectName(projectNameEdit->text()); + config->setCompilerPath(compilerPathEdit->text()); + config->setAssemblerPath(assemblerPathEdit->text()); + config->setLinkerPath(linkerPathEdit->text()); + + if (config->saveConfig(currentFilePath)) { + modified = false; + updateWindowTitle(); + statusBar()->showMessage("工程已保存:" + currentFilePath, 3000); + return true; + } else { + QMessageBox::critical(this, "错误", "无法保存工程文件"); + return false; + } +} + +bool MainWindow::saveProjectAs() +{ + QString filePath = QFileDialog::getSaveFileName(this, "保存工程", "", + "VSC 工程文件 (*.VSC);;所有文件 (*)"); + + if (filePath.isEmpty()) { + return false; + } + + currentFilePath = filePath; + return saveProject(); +} + +void MainWindow::generateCMake() +{ + // 先保存工程 + if (!saveProject()) { + return; + } + + // 获取工程目录(VSC 工程文件所在目录) + QString projectDir = QFileInfo(currentFilePath).absolutePath(); + QString buildDir = projectDir + "/build"; + QString cmakePath = projectDir + "/CMakeLists.txt"; + + // 删除旧的 build 目录 + QDir buildPath(buildDir); + if (buildPath.exists()) { + buildPath.removeRecursively(); + statusBar()->showMessage("已删除旧 build 目录", 2000); + } + + // 重新创建 build 目录 + buildPath.mkpath(buildDir); + + // 生成 CMake 文件 + if (cmakeGenerator->generate(config, cmakePath)) { + statusBar()->showMessage("CMake 已生成,正在编译...", 2000); + + // 在 build 目录执行 cmake + QProcess process; + process.setWorkingDirectory(buildDir); + + // 跨平台:Windows 必须指定 MinGW Makefiles 生成器 + QStringList cmakeArgs; +#ifdef Q_OS_WIN32 + cmakeArgs << "-G" << "MinGW Makefiles" << ".."; +#else + cmakeArgs << ".."; +#endif + process.start("cmake", cmakeArgs); + process.waitForFinished(10000); + + QString output = process.readAllStandardOutput(); + QString error = process.readAllStandardError(); + + if (process.exitCode() == 0) { + statusBar()->showMessage("CMake 配置成功!", 3000); + // 不弹窗了,编译按钮会直接打开编译窗口 + } else { + statusBar()->showMessage("CMake 配置失败", 3000); + QMessageBox::warning(this, "警告", + "CMake 配置完成但有警告:\n" + error); + } + } +} +void MainWindow::onProjectNameChanged(const QString &text) +{ + config->setProjectName(text); +} + +void MainWindow::onCompilerPathChanged() +{ + config->setCompilerPath(compilerPathEdit->text()); +} + +void MainWindow::onAssemblerPathChanged() +{ + config->setAssemblerPath(assemblerPathEdit->text()); +} + +void MainWindow::onLinkerPathChanged() +{ + config->setLinkerPath(linkerPathEdit->text()); +} + +void MainWindow::addVirtualDir() +{ + bool ok; + QString dirName = QInputDialog::getText(this, "添加虚拟目录", + "目录名称:", QLineEdit::Normal, "", &ok); + + if (ok && !dirName.isEmpty()) { + config->addVirtualDir(dirName); + + QTreeWidgetItem *item = new QTreeWidgetItem(sourceTree); + item->setText(0, dirName); + sourceTree->expandAll(); + } +} + +void MainWindow::addSourceFile() +{ + QTreeWidgetItem *currentItem = sourceTree->currentItem(); + if (!currentItem) { + QMessageBox::warning(this, "警告", "请先选择虚拟目录"); + return; + } + + QString dirName = currentItem->text(0); + + // 使用 JSON 文件所在目录作为初始目录 + QString initialDir = currentFilePath.isEmpty() ? "" : QFileInfo(currentFilePath).absolutePath(); + + QStringList files = QFileDialog::getOpenFileNames(this, "选择源文件", initialDir, + "C 源文件 (*.c);;所有文件 (*)"); + + for (const QString &file : files) { + config->addSourceFile(dirName, file); + + QTreeWidgetItem *fileItem = new QTreeWidgetItem(currentItem); + fileItem->setText(0, file); + } +} + +void MainWindow::removeSourceFile() +{ + QTreeWidgetItem *currentItem = sourceTree->currentItem(); + if (!currentItem) { + return; + } + + QString dirName = currentItem->parent() ? currentItem->parent()->text(0) : currentItem->text(0); + QString fileName = currentItem->text(0); + + if (currentItem->parent()) { + config->removeSourceFile(dirName, fileName); + delete currentItem; + } else { + config->clearVirtualDirs(); + delete currentItem; + } +} + +void MainWindow::addIncludeDir() +{ + // 使用 JSON 文件所在目录作为初始目录 + QString initialDir = currentFilePath.isEmpty() ? "" : QFileInfo(currentFilePath).absolutePath(); + + QString dir = QFileDialog::getExistingDirectory(this, "选择包含目录", initialDir); + if (!dir.isEmpty()) { + config->addIncludeDir(dir); + includeDirList->addItem(dir); + } +} + +void MainWindow::removeIncludeDir() +{ + int row = includeDirList->currentRow(); + if (row >= 0) { + QString dir = includeDirList->takeItem(row)->text(); + config->removeIncludeDir(dir); + } +} + +void MainWindow::addLibrary() +{ + // 使用 JSON 文件所在目录作为初始目录 + QString initialDir = currentFilePath.isEmpty() ? "" : QFileInfo(currentFilePath).absolutePath(); + + QStringList files = QFileDialog::getOpenFileNames(this, "选择库文件", initialDir, + "库文件 (*.a *.so *.lib *.dll);;所有文件 (*)"); + + for (const QString &file : files) { + config->addLibrary(file); + libraryList->addItem(file); + } +} + +void MainWindow::removeLibrary() +{ + int row = libraryList->currentRow(); + if (row >= 0) { + QString lib = libraryList->takeItem(row)->text(); + config->removeLibrary(lib); + } +} + +void MainWindow::addDefine() +{ + bool ok; + QString macro = QInputDialog::getText(this, "添加编译宏", + "宏定义 (例如 DEBUG):", QLineEdit::Normal, "", &ok); + + if (ok && !macro.isEmpty()) { + config->addDefine(macro); + defineList->addItem(macro); + } +} + +void MainWindow::removeDefine() +{ + int row = defineList->currentRow(); + if (row >= 0) { + QString macro = defineList->takeItem(row)->text(); + config->removeDefine(macro); + } +} + +void MainWindow::addCompilerOption() +{ + bool ok; + QString option = QInputDialog::getText(this, "添加编译选项", + "编译选项 (例如 -Wall):", QLineEdit::Normal, "", &ok); + + if (ok && !option.isEmpty()) { + config->addCompilerOption(option); + optionList->addItem(option); + } +} + +void MainWindow::removeCompilerOption() +{ + int row = optionList->currentRow(); + if (row >= 0) { + QString option = optionList->takeItem(row)->text(); + config->removeCompilerOption(option); + } +} + +void MainWindow::browseCompilerPath() +{ + QString path = QFileDialog::getOpenFileName(this, "选择编译器", "", + "可执行文件 (*)"); + if (!path.isEmpty()) { + compilerPathEdit->setText(path); + } +} + +void MainWindow::browseAssemblerPath() +{ + QString path = QFileDialog::getOpenFileName(this, "选择汇编器", "", + "可执行文件 (*)"); + if (!path.isEmpty()) { + assemblerPathEdit->setText(path); + } +} + +void MainWindow::browseLinkerPath() +{ + QString path = QFileDialog::getOpenFileName(this, "选择链接器", "", + "可执行文件 (*)"); + if (!path.isEmpty()) { + linkerPathEdit->setText(path); + } +} + +void MainWindow::compileDebug() +{ + showCMakeSuccessMsg = false; // 编译时不弹 CMake 成功窗 + compileProject("Debug"); +} + +void MainWindow::compileRelease() +{ + showCMakeSuccessMsg = false; // 编译时不弹 CMake 成功窗 + compileProject("Release"); +} + +void MainWindow::compileProject(const QString &buildType) +{ + // 先校验 + if (currentFilePath.isEmpty()) { + QMessageBox::warning(this, "警告", "请先保存工程!"); + return; + } + + QString projectDir = QFileInfo(currentFilePath).absolutePath(); + + // ★ 第一时间弹窗,让用户知道开始工作了 + QDialog *compileDialog = new QDialog(this); + compileDialog->setWindowTitle("编译 " + buildType); + compileDialog->resize(800, 600); + compileDialog->setAttribute(Qt::WA_DeleteOnClose); + + QVBoxLayout *dialogLayout = new QVBoxLayout(compileDialog); + + QLabel *titleLabel = new QLabel("🔨 正在准备编译 " + buildType + "..."); + titleLabel->setStyleSheet("font-size: 18px; font-weight: bold; color: #333;"); + dialogLayout->addWidget(titleLabel); + + QTextEdit *outputEdit = new QTextEdit(); + outputEdit->setReadOnly(true); + outputEdit->setFont(QFont("Consolas", 10)); + outputEdit->setStyleSheet("background: #1e1e1e; color: #d4d4d4;"); + dialogLayout->addWidget(outputEdit); + + QPushButton *closeBtn = new QPushButton("❌ 关闭"); + closeBtn->setMinimumHeight(35); + closeBtn->setStyleSheet("QPushButton { background: #C62828; color: white; font-weight: bold; font-size: 14px; border: none; border-radius: 4px; padding: 6px 16px; }"); + closeBtn->setEnabled(false); // 编译未完成时不可关闭 + dialogLayout->addWidget(closeBtn); + + // ★ 锁死编译按钮 + debugBtn->setEnabled(false); + debugBtn->setText("🔒 编译中..."); + releaseBtn->setEnabled(false); + releaseBtn->setText("🔒 编译中..."); + + // 弹窗关闭时解锁按钮 + connect(compileDialog, &QDialog::finished, this, [this]() { + debugBtn->setEnabled(true); + debugBtn->setText("🐛 编译 Debug"); + releaseBtn->setEnabled(true); + releaseBtn->setText("🚀 编译 Release"); + }); + + compileDialog->show(); + QApplication::processEvents(); // 立即渲染弹窗 + + // ===== 阶段 1:保存工程 ===== + outputEdit->append("💾 正在保存工程配置..."); + if (!saveProject()) { + outputEdit->append("❌ 保存失败!"); + titleLabel->setText("❌ 保存失败"); + titleLabel->setStyleSheet("font-size: 18px; font-weight: bold; color: #f44336;"); + closeBtn->setEnabled(true); + return; + } + outputEdit->append("✅ 工程已保存\n"); + QApplication::processEvents(); + + // ===== 阶段 2:清理旧 build ===== + QString buildDir = projectDir + "/build"; + outputEdit->append("🧹 正在清理旧的构建目录..."); + QDir buildPath(buildDir); + if (buildPath.exists()) { + buildPath.removeRecursively(); + outputEdit->append("✅ 已删除旧 build 目录\n"); + } else { + outputEdit->append("✅ build 目录不存在,跳过清理\n"); + } + buildPath.mkpath(buildDir); + QApplication::processEvents(); + + // ===== 阶段 3:生成 CMakeLists.txt ===== + outputEdit->append("📄 正在生成 CMakeLists.txt..."); + QString cmakePath = projectDir + "/CMakeLists.txt"; + if (!cmakeGenerator->generate(config, cmakePath)) { + outputEdit->append("❌ 生成 CMake 失败!"); + titleLabel->setText("❌ CMake 生成失败"); + titleLabel->setStyleSheet("font-size: 18px; font-weight: bold; color: #f44336;"); + closeBtn->setEnabled(true); + return; + } + outputEdit->append("✅ CMakeLists.txt 已生成\n"); + QApplication::processEvents(); + + // ===== 阶段 4:CMake 配置 ===== + outputEdit->append("📋 正在配置 CMake..."); + QProcess cmakeProcess; + cmakeProcess.setWorkingDirectory(buildDir); + + QStringList cmakeArgs; +#ifdef Q_OS_WIN32 + cmakeArgs << "-G" << "MinGW Makefiles" << "-DCMAKE_BUILD_TYPE=" + buildType << ".."; +#else + cmakeArgs << "-DCMAKE_BUILD_TYPE=" + buildType << ".."; +#endif + cmakeProcess.start("cmake", cmakeArgs); + cmakeProcess.waitForFinished(30000); + + QString cmakeOutput = cmakeProcess.readAllStandardOutput(); + if (!cmakeOutput.isEmpty()) { + outputEdit->append(cmakeOutput); + } + QString cmakeError = cmakeProcess.readAllStandardError(); + if (!cmakeError.isEmpty()) { + outputEdit->append("⚠️ CMake 警告/错误:\n" + cmakeError); + } + + if (cmakeProcess.exitCode() != 0) { + outputEdit->append("\n❌ CMake 配置失败!错误代码:" + QString::number(cmakeProcess.exitCode())); + titleLabel->setText("❌ CMake 配置失败"); + titleLabel->setStyleSheet("font-size: 18px; font-weight: bold; color: #f44336;"); + closeBtn->setEnabled(true); + return; + } + outputEdit->append("✅ CMake 配置完成\n"); + QApplication::processEvents(); + + // ===== 阶段 5:编译 ===== + outputEdit->append("🔨 正在编译 " + buildType + "...\n"); + titleLabel->setText("🔨 编译 " + buildType + " 中..."); + QApplication::processEvents(); + + QProcess makeProcess; + makeProcess.setWorkingDirectory(buildDir); + +#ifdef Q_OS_WIN32 + QString makeCmd = "mingw32-make"; +#else + QString makeCmd = "make"; +#endif + makeProcess.start(makeCmd, QStringList() << buildType.toLower()); + + // 实时输出 + connect(&makeProcess, &QProcess::readyReadStandardOutput, [&outputEdit, &makeProcess]() { + outputEdit->append(makeProcess.readAllStandardOutput()); + outputEdit->verticalScrollBar()->setValue(outputEdit->verticalScrollBar()->maximum()); + }); + + connect(&makeProcess, &QProcess::readyReadStandardError, [&outputEdit, &makeProcess]() { + outputEdit->append(makeProcess.readAllStandardError()); + outputEdit->verticalScrollBar()->setValue(outputEdit->verticalScrollBar()->maximum()); + }); + + makeProcess.waitForFinished(300000); // 5 分钟超时 + + if (makeProcess.exitCode() == 0) { + outputEdit->append("\n✅ 编译成功!"); + titleLabel->setText("✅ 编译 " + buildType + " 成功!"); + titleLabel->setStyleSheet("font-size: 18px; font-weight: bold; color: #4CAF50;"); + } else { + outputEdit->append("\n❌ 编译失败!错误代码:" + QString::number(makeProcess.exitCode())); + titleLabel->setText("❌ 编译 " + buildType + " 失败!"); + titleLabel->setStyleSheet("font-size: 18px; font-weight: bold; color: #f44336;"); + } + + // ★ 编译完成,允许关闭和重新编译 + closeBtn->setEnabled(true); + connect(closeBtn, &QPushButton::clicked, compileDialog, &QDialog::accept); +} + diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 index 0000000..016c4c4 --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,105 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "projectconfig.h" +#include "cmakegenerator.h" + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + // 通过命令行/双击打开工程文件 + void openProjectFile(const QString &filePath); + +private slots: + // 文件操作 + void newProject(); + void openProject(); + bool saveProject(); + bool saveProjectAs(); + + // 生成 CMake + void generateCMake(); + + // 编译 + void compileDebug(); + void compileRelease(); + void compileProject(const QString &buildType); + + // UI 更新 + void onProjectNameChanged(const QString &text); + void onCompilerPathChanged(); + void onAssemblerPathChanged(); + void onLinkerPathChanged(); + + // 虚拟目录操作 + void addVirtualDir(); + void addSourceFile(); + void removeSourceFile(); + + // 包含目录操作 + void addIncludeDir(); + void removeIncludeDir(); + + // 库文件操作 + void addLibrary(); + void removeLibrary(); + + // 编译宏操作 + void addDefine(); + void removeDefine(); + + // 编译选项操作 + void addCompilerOption(); + void removeCompilerOption(); + + // 浏览文件 + void browseCompilerPath(); + void browseAssemblerPath(); + void browseLinkerPath(); + +private: + void setupUI(); + void setupConnections(); + void updateWindowTitle(); + bool maybeSave(); + + // 组件 + QLineEdit *projectNameEdit; + QLineEdit *compilerPathEdit; + QLineEdit *assemblerPathEdit; + QLineEdit *linkerPathEdit; + + QTreeWidget *sourceTree; // 虚拟目录树 + QListWidget *includeDirList; // 包含目录列表 + QListWidget *libraryList; // 库文件列表 + QListWidget *defineList; // 编译宏列表 + QListWidget *optionList; // 编译选项列表 + + QPushButton *saveButton; + QPushButton *generateButton; + QPushButton *debugBtn; + QPushButton *releaseBtn; + + // 配置和生成器 + ProjectConfig *config; + CMakeGenerator *cmakeGenerator; + + QString currentFilePath; + bool modified; + bool showCMakeSuccessMsg; // 控制是否显示 CMake 成功弹窗 +}; + +#endif // MAINWINDOW_H diff --git a/src/projectconfig.cpp b/src/projectconfig.cpp new file mode 100644 index 0000000..29010a0 --- /dev/null +++ b/src/projectconfig.cpp @@ -0,0 +1,351 @@ +#include "projectconfig.h" +#include +#include +#include +#include +#include + +ProjectConfig::ProjectConfig(QObject *parent) + : QObject(parent) +{ + m_projectName = "Untitled"; + m_projectPath = ""; + m_compilerPath = "gcc"; + m_assemblerPath = "gcc"; + m_linkerPath = "gcc"; + m_outputDir = "./build"; +} + +bool ProjectConfig::loadConfig(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + file.close(); + + if (doc.isNull()) { + return false; + } + + fromJson(doc.object()); + return true; +} + +bool ProjectConfig::saveConfig(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) { + return false; + } + + QJsonDocument doc(toJson()); + file.write(doc.toJson()); + file.close(); + + return true; +} + +QJsonObject ProjectConfig::toJson() const +{ + QJsonObject json; + + // 基本信息 + json["projectName"] = m_projectName; + json["projectPath"] = m_projectPath; + json["outputDir"] = m_outputDir; + + // 工具链 + json["compilerPath"] = m_compilerPath; + json["assemblerPath"] = m_assemblerPath; + json["linkerPath"] = m_linkerPath; + + // 虚拟目录 + QJsonObject virtualDirs; + for (auto it = m_virtualDirs.begin(); it != m_virtualDirs.end(); ++it) { + QJsonObject dirObj; + dirObj["name"] = it->name; + QJsonArray files; + for (const QString &f : it->files) { + files.append(f); + } + dirObj["files"] = files; + virtualDirs[it.key()] = dirObj; + } + json["virtualDirs"] = virtualDirs; + + // 包含目录 + QJsonArray includeDirs; + for (const QString &dir : m_includeDirs) { + includeDirs.append(dir); + } + json["includeDirs"] = includeDirs; + + // 库文件 + QJsonArray libraries; + for (const QString &lib : m_libraries) { + libraries.append(lib); + } + json["libraries"] = libraries; + + // 编译宏 + QJsonArray defines; + for (const QString &def : m_defines) { + defines.append(def); + } + json["defines"] = defines; + + // 编译选项 + QJsonArray options; + for (const QString &opt : m_compilerOptions) { + options.append(opt); + } + json["compilerOptions"] = options; + + return json; +} + +void ProjectConfig::fromJson(const QJsonObject &json) +{ + m_projectName = json["projectName"].toString(); + m_projectPath = json["projectPath"].toString(); + m_outputDir = json["outputDir"].toString(); + m_compilerPath = json["compilerPath"].toString(); + m_assemblerPath = json["assemblerPath"].toString(); + m_linkerPath = json["linkerPath"].toString(); + + // 虚拟目录 + m_virtualDirs.clear(); + QJsonObject virtualDirs = json["virtualDirs"].toObject(); + for (auto it = virtualDirs.begin(); it != virtualDirs.end(); ++it) { + VirtualDir dir; + dir.name = it->toObject()["name"].toString(); + QJsonArray files = it->toObject()["files"].toArray(); + for (const QJsonValue &f : files) { + dir.files.append(f.toString()); + } + m_virtualDirs[it.key()] = dir; + } + + // 包含目录 + m_includeDirs.clear(); + QJsonArray includeDirs = json["includeDirs"].toArray(); + for (const QJsonValue &dir : includeDirs) { + m_includeDirs.append(dir.toString()); + } + + // 库文件 + m_libraries.clear(); + QJsonArray libraries = json["libraries"].toArray(); + for (const QJsonValue &lib : libraries) { + m_libraries.append(lib.toString()); + } + + // 编译宏 + m_defines.clear(); + QJsonArray defines = json["defines"].toArray(); + for (const QJsonValue &def : defines) { + m_defines.append(def.toString()); + } + + // 编译选项 + m_compilerOptions.clear(); + QJsonArray options = json["compilerOptions"].toArray(); + for (const QJsonValue &opt : options) { + m_compilerOptions.append(opt.toString()); + } + + emit configChanged(); +} + +// Getter/Setter 实现 +void ProjectConfig::setProjectName(const QString &name) { + if (m_projectName != name) { + m_projectName = name; + emit configChanged(); + } +} + +QString ProjectConfig::getProjectName() const { + return m_projectName; +} + +void ProjectConfig::setProjectPath(const QString &path) { + if (m_projectPath != path) { + m_projectPath = path; + emit configChanged(); + } +} + +QString ProjectConfig::getProjectPath() const { + return m_projectPath; +} + +void ProjectConfig::setCompilerPath(const QString &path) { + if (m_compilerPath != path) { + m_compilerPath = path; + emit configChanged(); + } +} + +QString ProjectConfig::getCompilerPath() const { + return m_compilerPath; +} + +void ProjectConfig::setAssemblerPath(const QString &path) { + if (m_assemblerPath != path) { + m_assemblerPath = path; + emit configChanged(); + } +} + +QString ProjectConfig::getAssemblerPath() const { + return m_assemblerPath; +} + +void ProjectConfig::setLinkerPath(const QString &path) { + if (m_linkerPath != path) { + m_linkerPath = path; + emit configChanged(); + } +} + +QString ProjectConfig::getLinkerPath() const { + return m_linkerPath; +} + +void ProjectConfig::setOutputDir(const QString &dir) { + if (m_outputDir != dir) { + m_outputDir = dir; + emit configChanged(); + } +} + +QString ProjectConfig::getOutputDir() const { + return m_outputDir; +} + +// 虚拟目录操作 +void ProjectConfig::addVirtualDir(const QString &dirName) { + if (!m_virtualDirs.contains(dirName)) { + VirtualDir dir; + dir.name = dirName; + m_virtualDirs[dirName] = dir; + emit configChanged(); + } +} + +void ProjectConfig::addSourceFile(const QString &dirName, const QString &filePath) { + if (m_virtualDirs.contains(dirName)) { + if (!m_virtualDirs[dirName].files.contains(filePath)) { + m_virtualDirs[dirName].files.append(filePath); + emit configChanged(); + } + } +} + +void ProjectConfig::removeSourceFile(const QString &dirName, const QString &filePath) { + if (m_virtualDirs.contains(dirName)) { + m_virtualDirs[dirName].files.removeAll(filePath); + emit configChanged(); + } +} + +QMap ProjectConfig::getVirtualDirs() const { + return m_virtualDirs; +} + +void ProjectConfig::clearVirtualDirs() { + m_virtualDirs.clear(); + emit configChanged(); +} + +// 包含目录操作 +void ProjectConfig::addIncludeDir(const QString &dir) { + if (!m_includeDirs.contains(dir)) { + m_includeDirs.append(dir); + emit configChanged(); + } +} + +void ProjectConfig::removeIncludeDir(const QString &dir) { + m_includeDirs.removeAll(dir); + emit configChanged(); +} + +QStringList ProjectConfig::getIncludeDirs() const { + return m_includeDirs; +} + +void ProjectConfig::clearIncludeDirs() { + m_includeDirs.clear(); + emit configChanged(); +} + +// 库文件操作 +void ProjectConfig::addLibrary(const QString &libPath) { + if (!m_libraries.contains(libPath)) { + m_libraries.append(libPath); + emit configChanged(); + } +} + +void ProjectConfig::removeLibrary(const QString &libPath) { + m_libraries.removeAll(libPath); + emit configChanged(); +} + +QStringList ProjectConfig::getLibraries() const { + return m_libraries; +} + +void ProjectConfig::clearLibraries() { + m_libraries.clear(); + emit configChanged(); +} + +// 编译宏操作 +void ProjectConfig::addDefine(const QString ¯o) { + if (!m_defines.contains(macro)) { + m_defines.append(macro); + emit configChanged(); + } +} + +void ProjectConfig::removeDefine(const QString ¯o) { + m_defines.removeAll(macro); + emit configChanged(); +} + +QStringList ProjectConfig::getDefines() const { + return m_defines; +} + +void ProjectConfig::clearDefines() { + m_defines.clear(); + emit configChanged(); +} + +// 编译选项操作 +void ProjectConfig::addCompilerOption(const QString &option) { + if (!m_compilerOptions.contains(option)) { + m_compilerOptions.append(option); + emit configChanged(); + } +} + +void ProjectConfig::removeCompilerOption(const QString &option) { + m_compilerOptions.removeAll(option); + emit configChanged(); +} + +QStringList ProjectConfig::getCompilerOptions() const { + return m_compilerOptions; +} + +void ProjectConfig::clearCompilerOptions() { + m_compilerOptions.clear(); + emit configChanged(); +} diff --git a/src/projectconfig.h b/src/projectconfig.h new file mode 100644 index 0000000..19cb42f --- /dev/null +++ b/src/projectconfig.h @@ -0,0 +1,101 @@ +#ifndef PROJECTCONFIG_H +#define PROJECTCONFIG_H + +#include +#include +#include +#include +#include + +// 虚拟目录结构 +struct VirtualDir { + QString name; + QStringList files; // .c 文件列表 +}; + +class ProjectConfig : public QObject +{ + Q_OBJECT + +public: + explicit ProjectConfig(QObject *parent = nullptr); + + // 加载/保存配置 + bool loadConfig(const QString &filePath); + bool saveConfig(const QString &filePath); + + // 工程基本信息 + void setProjectName(const QString &name); + QString getProjectName() const; + + void setProjectPath(const QString &path); + QString getProjectPath() const; + + // 编译工具链 + void setCompilerPath(const QString &path); + QString getCompilerPath() const; + + void setAssemblerPath(const QString &path); + QString getAssemblerPath() const; + + void setLinkerPath(const QString &path); + QString getLinkerPath() const; + + // 输出目录 + void setOutputDir(const QString &dir); + QString getOutputDir() const; + + // 虚拟目录(.c 文件) + void addVirtualDir(const QString &dirName); + void addSourceFile(const QString &dirName, const QString &filePath); + void removeSourceFile(const QString &dirName, const QString &filePath); + QMap getVirtualDirs() const; + void clearVirtualDirs(); + + // 包含目录(.h 文件) + void addIncludeDir(const QString &dir); + void removeIncludeDir(const QString &dir); + QStringList getIncludeDirs() const; + void clearIncludeDirs(); + + // 库文件 + void addLibrary(const QString &libPath); + void removeLibrary(const QString &libPath); + QStringList getLibraries() const; + void clearLibraries(); + + // 编译宏 + void addDefine(const QString ¯o); + void removeDefine(const QString ¯o); + QStringList getDefines() const; + void clearDefines(); + + // 自定义编译选项 + void addCompilerOption(const QString &option); + void removeCompilerOption(const QString &option); + QStringList getCompilerOptions() const; + void clearCompilerOptions(); + + // 转换为 JSON + QJsonObject toJson() const; + void fromJson(const QJsonObject &json); + +signals: + void configChanged(); + +private: + QString m_projectName; + QString m_projectPath; + QString m_compilerPath; + QString m_assemblerPath; + QString m_linkerPath; + QString m_outputDir; + + QMap m_virtualDirs; // 虚拟目录 + QStringList m_includeDirs; // 包含目录 + QStringList m_libraries; // 库文件 + QStringList m_defines; // 编译宏 + QStringList m_compilerOptions; // 自定义编译选项 +}; + +#endif // PROJECTCONFIG_H diff --git a/需求规格说明书.md b/需求规格说明书.md new file mode 100644 index 0000000..20d2bfb --- /dev/null +++ b/需求规格说明书.md @@ -0,0 +1,542 @@ +# VoiletCStudio 需求规格说明书 + +**版本号:** v1.1 +**项目名称:** 紫罗兰 C 工程配置器(VoiletCStudio) +**开发日期:** 2026-04-09 +**最后更新:** 2026-04-28 +**作者:** 虾哥 + +--- + +## 📋 目录 + +1. [项目概述](#1-项目概述) +2. [功能需求](#2-功能需求) +3. [界面设计](#3-界面设计) +4. [技术规格](#4-技术规格) +5. [使用说明](#5-使用说明) +6. [文件结构](#6-文件结构) +7. [配置文件格式](#7-配置文件格式) +8. [编译流程](#8-编译流程) + +--- + +## 1. 项目概述 + +### 1.1 项目背景 + +VoiletCStudio 是一款跨平台的 C 工程配置工具,旨在简化 C 语言项目的构建流程。通过图形化界面管理工程配置,自动生成 CMakeLists.txt 文件,支持一键编译 Debug 和 Release 版本。 + +### 1.2 目标用户 + +- C 语言开发者 +- 嵌入式系统开发人员 +- 需要跨平台编译的项目团队 +- 从 IDE 迁移到 CMake 的开发者 + +### 1.3 核心价值 + +- **简化配置**:图形化界面替代手动编写 CMakeLists.txt +- **跨平台支持**:Windows (MinGW) / Linux (GCC) / macOS (Clang) +- **提高效率**:一键生成和编译,减少重复劳动 +- **易于上手**:类似 MDK 的虚拟目录管理,降低学习成本 + +--- + +## 2. 功能需求 + +### 2.1 工程文件管理 + +| 功能 ID | 功能名称 | 功能描述 | 优先级 | +|--------|---------|---------|--------| +| F-001 | 新建工程 | 创建新的 C 工程配置 | 高 | +| F-002 | 打开工程 | 加载已保存的 JSON 配置文件 | 高 | +| F-003 | 保存工程 | 保存当前配置到 JSON 文件 | 高 | +| F-004 | 另存为 | 将工程保存到指定位置 | 中 | +| F-005 | 拖拽打开 | 拖拽 JSON 文件到窗口打开工程 | 中 | + +### 2.2 编译工具链配置 + +| 功能 ID | 功能名称 | 功能描述 | 优先级 | +|--------|---------|---------|--------| +| F-010 | 编译器配置 | 设置 C 编译器路径(gcc/clang 等) | 高 | +| F-011 | 汇编器配置 | 设置汇编器路径 | 中 | +| F-012 | 链接器配置 | 设置链接器路径 | 中 | + +### 2.3 文件管理 + +| 功能 ID | 功能名称 | 功能描述 | 优先级 | +|--------|---------|---------|--------| +| F-020 | 虚拟目录 | 创建类似 MDK 的虚拟文件夹 | 高 | +| F-021 | 源文件管理 | 添加/移除 .c 源文件 | 高 | +| F-022 | 包含目录 | 添加/移除 .h 头文件目录 | 高 | +| F-023 | 库文件管理 | 添加/移除 .a/.so/.lib 库文件 | 高 | + +### 2.4 编译配置 + +| 功能 ID | 功能名称 | 功能描述 | 优先级 | +|--------|---------|---------|--------| +| F-030 | 编译宏 | 添加/移除预编译宏定义 | 高 | +| F-031 | 编译选项 | 添加/移除自定义编译参数 | 中 | + +### 2.5 CMake 生成 + +| 功能 ID | 功能名称 | 功能描述 | 优先级 | +|--------|---------|---------|--------| +| F-040 | 生成 CMake | 自动生成 CMakeLists.txt 文件 | 高 | +| F-041 | 命令提示 | 显示编译命令使用说明 | 高 | + +### 2.6 编译功能 + +| 功能 ID | 功能名称 | 功能描述 | 优先级 | +|--------|---------|---------|--------| +| F-050 | 编译 Debug | 编译 Debug 版本(带调试信息) | 高 | +| F-051 | 编译 Release | 编译 Release 版本(优化发布) | 高 | +| F-052 | 实时输出 | 显示编译过程实时日志 | 高 | +| F-053 | 自动清理 | 编译前自动清空 build 目录 | 高 | + +--- + +## 3. 界面设计 + +### 3.1 主界面布局 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VoiletCStudio - C 工程配置器 │ +├─────────────────────────────────────────────────────────────┤ +│ ⚡ 快捷操作 │ +│ [📂 打开] [📄 新建] [💾 保存] [🔨 生成 CMake] [🐛 编译 Debug] [🚀 编译 Release] │ +├─────────────────────────────────────────────────────────────┤ +│ 📁 工程基本信息 │ +│ 工程名:[____________] │ +├─────────────────────────────────────────────────────────────┤ +│ 🔧 编译工具链 │ +│ 编译器:[____] 汇编器:[____] 链接器:[____] │ +├─────────────────────────────────────────────────────────────┤ +│ 📁 源文件 │ 📦 库文件 │ +│ ├─ 📁 App │ ├─ libmylib.a │ +│ │ ├─ main.c │ 🏷️ 编译宏 │ +│ │ └─ utils.c │ ├─ DEBUG │ +│ 📚 包含目录 │ ⚙️ 编译选项 │ +│ ├─ ./include │ ├─ -Wall │ +├─────────────────────────────────────────────────────────────┤ +│ 就绪 [状态栏] │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 按钮说明 + +| 按钮 | 颜色 | 功能 | 快捷键 | +|------|------|------|--------| +| 📂 打开工程 | 🔵 蓝色 | 打开已有工程 | Ctrl+O | +| 📄 新建工程 | 🟢 绿色 | 创建新工程 | Ctrl+N | +| 💾 保存工程 | 🟠 橙色 | 保存配置 | Ctrl+S | +| 🔨 生成 CMake | 🟣 紫色 | 生成 CMakeLists.txt | Ctrl+G | +| 🐛 编译 Debug | 🔵 青色 | 编译 Debug 版本 | - | +| 🚀 编译 Release | 🌸 粉色 | 编译 Release 版本 | - | + +--- + +## 4. 技术规格 + +### 4.1 开发环境 + +| 项目 | 规格 | +|------|------| +| 开发语言 | C++ 11 | +| GUI 框架 | Qt 5 | +| 构建系统 | qmake | +| 目标平台 | Windows / Linux / macOS | + +### 4.2 系统要求 + +| 项目 | 最低要求 | 推荐配置 | +|------|---------|---------| +| 操作系统 | Windows 7 / Ubuntu 18.04 / macOS 10.14 | Windows 10 / Ubuntu 20.04 / macOS 11+ | +| 内存 | 512 MB | 2 GB | +| 磁盘空间 | 100 MB | 500 MB | +| 编译器 | GCC 5.0+ / MinGW / Clang | GCC 9.0+ / MinGW-w64 / Clang 10+ | + +### 4.3 依赖库 + +| 库名称 | 版本 | 用途 | +|--------|------|------| +| Qt Core | 5.12+ | 核心功能 | +| Qt GUI | 5.12+ | 图形界面 | +| Qt Widgets | 5.12+ | 窗口组件 | +| CMake | 3.10+ | 构建系统 | +| GCC/MinGW | 5.0+ | C 编译器 | + +--- + +## 5. 使用说明 + +### 5.1 快速开始 + +#### 步骤 1:新建工程 + +1. 点击【📄 新建工程】按钮 +2. 输入工程名称 +3. 点击【💾 保存工程】,选择保存位置 + +#### 步骤 2:配置工具链 + +1. 点击编译器旁的【浏览】按钮 +2. 选择编译器路径(如 `/usr/bin/gcc`) +3. 同样配置汇编器和链接器(通常与编译器相同) + +#### 步骤 3:添加源文件 + +1. 点击【➕ 目录】创建虚拟文件夹(如 "App") +2. 选中目录,点击【➕ 文件】添加 .c 文件 +3. 重复添加所有源文件 + +#### 步骤 4:配置包含目录 + +1. 切换到右侧"包含目录"面板 +2. 点击【➕ 添加】选择头文件目录 +3. 添加所有需要的包含路径 + +#### 步骤 5:添加库文件(可选) + +1. 切换到"库文件"面板 +2. 点击【➕ 添加】选择 .a/.so/.lib 文件 +3. 添加所有依赖库 + +#### 步骤 6:配置编译选项 + +1. 切换到"编译宏"面板 +2. 点击【➕ 添加】输入宏定义(如 `DEBUG`) +3. 切换到"编译选项"面板 +4. 添加自定义编译参数(如 `-Wall`) + +#### 步骤 7:生成 CMake + +1. 点击【🔨 生成 CMake】按钮 +2. 查看弹出的编译命令提示 +3. CMakeLists.txt 已生成到工程目录 + +#### 步骤 8:编译工程 + +**方式一:使用内置编译** +1. 点击【🐛 编译 Debug】或【🚀 编译 Release】 +2. 查看编译输出窗口 +3. 编译完成后查看结果 + +**方式二:手动编译** +```bash +cd build +make debug # 编译 Debug 版本 +make release # 编译 Release 版本 +``` + +--- + +## 6. 文件结构 + +### 6.1 工程目录结构 + +``` +MyProject/ +├── config.json # 工程配置文件 +├── CMakeLists.txt # CMake 配置文件(自动生成) +├── build/ # 编译输出目录(自动生成) +│ ├── Makefile # Makefile(自动生成) +│ ├── debug/ # Debug 编译产物 +│ │ └── MyProject # Debug 可执行文件 +│ └── release/ # Release 编译产物 +│ └── MyProject # Release 可执行文件 +├── src/ # 源文件目录(用户创建) +│ ├── main.c +│ └── utils.c +└── include/ # 头文件目录(用户创建) + └── utils.h +``` + +### 6.2 源代码结构 + +``` +VoiletCStudio/ +├── src/ +│ ├── main.cpp # 程序入口 +│ ├── mainwindow.cpp/h # 主窗口实现 +│ ├── projectconfig.cpp/h # 配置管理 +│ └── cmakegenerator.cpp/h # CMake 生成 +├── VoiletCStudio.pro # Qt 项目文件 +├── README.md # 使用说明 +└── 需求规格说明书.md # 本文档 +``` + +--- + +## 7. 配置文件格式 + +### 7.1 JSON 配置示例 + +```json +{ + "projectName": "MyProject", + "projectPath": "/home/anonymous/Desktop/MyProject", + "compilerPath": "/usr/bin/gcc", + "assemblerPath": "/usr/bin/gcc", + "linkerPath": "/usr/bin/gcc", + "outputDir": "./build", + "virtualDirs": { + "App": { + "name": "App", + "files": [ + "/home/anonymous/Desktop/MyProject/src/main.c", + "/home/anonymous/Desktop/MyProject/src/utils.c" + ] + }, + "Drivers": { + "name": "Drivers", + "files": [ + "/home/anonymous/Desktop/MyProject/src/drivers/driver.c" + ] + } + }, + "includeDirs": [ + "/home/anonymous/Desktop/MyProject/include" + ], + "libraries": [ + "/usr/lib/libmylib.a" + ], + "defines": [ + "DEBUG", + "VERSION=1.0" + ], + "compilerOptions": [ + "-Wall", + "-Wextra" + ] +} +``` + +### 7.2 字段说明 + +| 字段名 | 类型 | 说明 | 必填 | +|--------|------|------|------| +| projectName | string | 工程名称 | 是 | +| projectPath | string | 工程路径 | 是 | +| compilerPath | string | 编译器路径 | 是 | +| assemblerPath | string | 汇编器路径 | 否 | +| linkerPath | string | 链接器路径 | 否 | +| outputDir | string | 输出目录 | 否 | +| virtualDirs | object | 虚拟目录配置 | 是 | +| includeDirs | array | 包含目录列表 | 否 | +| libraries | array | 库文件列表 | 否 | +| defines | array | 编译宏列表 | 否 | +| compilerOptions | array | 编译选项列表 | 否 | + +--- + +## 8. 编译流程 + +### 8.1 CMake 生成流程 + +``` +用户点击【生成 CMake】 + ↓ +读取工程配置(JSON) + ↓ +生成 CMakeLists.txt + ↓ +显示编译命令提示 + ↓ +完成 +``` + +### 8.2 编译执行流程 + +``` +用户点击【编译 Debug/Release】 + ↓ +保存工程配置 + ↓ +删除旧的 build 目录 + ↓ +创建新的 build 目录 + ↓ +生成 CMakeLists.txt + ↓ +执行 cmake -DCMAKE_BUILD_TYPE=Debug/Release .. + ↓ +执行 make debug/release + ↓ +实时显示编译输出 + ↓ +显示编译结果 + ↓ +完成 +``` + +### 8.3 CMakeLists.txt 结构 + +```cmake +# ======================================== +# CMakeLists.txt - VoiletCStudio 自动生成 +# 工程名称:MyProject +# 生成时间:2026-04-09 10:00:00 +# 跨平台支持:Windows (MinGW) / Linux (GCC) / macOS (Clang) +# ======================================== + +cmake_minimum_required(VERSION 3.10) + +# 项目名称 +project(MyProject C) + +# C 标准 +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# 输出目录配置 +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/./build) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/./build) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/./build) + +# 编译器配置 +set(CMAKE_C_COMPILER "/usr/bin/gcc") + +# ======================================== +# 源文件配置(虚拟目录结构) +# ======================================== + +# 分组:App +set(SRCS_App + /home/anonymous/Desktop/MyProject/src/main.c + /home/anonymous/Desktop/MyProject/src/utils.c +) + +# 分组:Drivers +set(SRCS_Drivers + /home/anonymous/Desktop/MyProject/src/drivers/driver.c +) + +# 合并所有源文件 +set(SOURCES + /home/anonymous/Desktop/MyProject/src/main.c + /home/anonymous/Desktop/MyProject/src/utils.c + /home/anonymous/Desktop/MyProject/src/drivers/driver.c +) + +# 创建可执行文件 +add_executable(MyProject ${SOURCES}) + +# 包含目录 +target_include_directories(MyProject PRIVATE + /home/anonymous/Desktop/MyProject/include +) + +# 链接库 +target_link_libraries(MyProject PRIVATE + /usr/lib/libmylib.a +) + +# 预编译宏定义 +target_compile_definitions(MyProject PRIVATE + DEBUG + VERSION=1.0 +) + +# 自定义编译选项 +target_compile_options(MyProject PRIVATE + -Wall + -Wextra +) + +# ======================================== +# 构建类型配置 +# ======================================== + +# 默认构建类型:Debug +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE) +endif() + +# Debug 模式配置(自动添加 DEBUG 宏) +set(CMAKE_C_FLAGS_DEBUG "-g -O0 -DDEBUG" CACHE STRING "Debug flags" FORCE) + +# Release 模式配置 +set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG" CACHE STRING "Release flags" FORCE) + +# ======================================== +# 自定义 Make 目标:make debug / make release +# ======================================== + +# make debug - 编译 Debug 版本(自动添加 DEBUG 宏) +add_custom_target(debug + COMMAND ${CMAKE_COMMAND} -DCMAKE_BUILD_TYPE=Debug ${CMAKE_SOURCE_DIR} + COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --config Debug + COMMENT "Building Debug version" + VERBATIM +) + +# make release - 编译 Release 版本 +add_custom_target(release + COMMAND ${CMAKE_COMMAND} -DCMAKE_BUILD_TYPE=Release ${CMAKE_SOURCE_DIR} + COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --config Release + COMMENT "Building Release version" + VERBATIM +) + +# 使用说明: +# cmake -B build # 配置(默认 Debug) +# cmake --build build # 编译(使用当前 CMAKE_BUILD_TYPE) +# make debug # 编译 Debug 版本(自动添加 DEBUG 宏) +# make release # 编译 Release 版本 +``` + +--- + +## 9. 常见问题 + +### 9.1 编译失败 + +**问题:** 点击编译后提示找不到编译器 + +**解决:** +1. 检查编译器路径是否正确 +2. 确认编译器已安装(`gcc --version`) +3. Windows 用户确认 MinGW 已正确安装 + +### 9.2 CMake 生成失败 + +**问题:** 点击生成 CMake 无反应 + +**解决:** +1. 检查工程是否已保存 +2. 确认保存路径有写入权限 +3. 查看状态栏错误提示 + +### 9.3 拖拽无反应 + +**问题:** 拖拽 JSON 文件到窗口没反应 + +**解决:** +1. 确认文件扩展名为 `.json` +2. 确认文件是有效的工程配置 +3. 尝试使用"打开工程"按钮 + +--- + +## 10. 版本历史 + +| 版本 | 日期 | 更新内容 | +|------|------|---------| +| v1.0 | 2026-04-09 | 初始版本,完整功能发布 | +| v1.1 | 2026-04-28 | 修复 Windows 跨平台兼容性:CMAKE_C_COMPILER 设置提前到 project() 之前;自动添加 MinGW Makefiles 生成器;mingw32-make 替代 make;按钮高对比度配色优化 | + +--- + +## 11. 联系方式 + +- **作者:** 虾哥 +- **邮箱:** anonymous@linuxacme.com +- **项目地址:** https://git.linuxacme.com/iorebuild/VoiletCStudio.git + +--- + +**文档版本:** v1.0 +**最后更新:** 2026-04-09