From 4627343e311b70fdea898eeab21a9e4160ddc645 Mon Sep 17 00:00:00 2001 From: luochen570 <1160510664@qq.com> Date: Sat, 30 May 2026 17:18:55 +0800 Subject: [PATCH] feat: add simplified Qt image viewer Dev branch --- .../workflows/build-windows-cross-linux.yml | 219 ---------- .../workflows/build-windows-msys2-release.yml | 180 -------- .gitea/workflows/build-windows-release.yml | 108 ----- .gitignore | 52 --- CMakeLists.txt | 63 +-- MainWindow.cpp | 413 ++++++++++++++++++ MainWindow.h | 90 ++++ README.md | 135 ------ app/include/ImageConverter.h | 11 - app/include/ImageProcessor.h | 23 - app/include/ImageView.h | 35 -- app/include/ImageWorker.h | 33 -- app/include/MainWindow.h | 72 --- app/src/ImageConverter.cpp | 47 -- app/src/ImageProcessor.cpp | 149 ------- app/src/ImageView.cpp | 89 ---- app/src/ImageWorker.cpp | 35 -- app/src/MainWindow.cpp | 369 ---------------- app/src/main.cpp | 20 - main.cpp | 16 + readme.md | 55 +++ 21 files changed, 591 insertions(+), 1623 deletions(-) delete mode 100644 .gitea/workflows/build-windows-cross-linux.yml delete mode 100644 .gitea/workflows/build-windows-msys2-release.yml delete mode 100644 .gitea/workflows/build-windows-release.yml delete mode 100644 .gitignore create mode 100644 MainWindow.cpp create mode 100644 MainWindow.h delete mode 100644 README.md delete mode 100644 app/include/ImageConverter.h delete mode 100644 app/include/ImageProcessor.h delete mode 100644 app/include/ImageView.h delete mode 100644 app/include/ImageWorker.h delete mode 100644 app/include/MainWindow.h delete mode 100644 app/src/ImageConverter.cpp delete mode 100644 app/src/ImageProcessor.cpp delete mode 100644 app/src/ImageView.cpp delete mode 100644 app/src/ImageWorker.cpp delete mode 100644 app/src/MainWindow.cpp delete mode 100644 app/src/main.cpp create mode 100644 main.cpp create mode 100644 readme.md diff --git a/.gitea/workflows/build-windows-cross-linux.yml b/.gitea/workflows/build-windows-cross-linux.yml deleted file mode 100644 index 025ce13..0000000 --- a/.gitea/workflows/build-windows-cross-linux.yml +++ /dev/null @@ -1,219 +0,0 @@ -name: Cross Build Windows Release on Linux - -on: - push: - branches: - - main - tags: - - 'v*' - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - cross-build-windows: - # 使用可访问 GitHub 的 Gitea act_runner(runner-mihomo),可直接拉取 actions/checkout、actions/upload-artifact 等 GitHub Action。 - runs-on: ubuntu-latest - container: - image: fedora:latest - - steps: - - name: Prepare action runtime - run: | - set -euo pipefail - sed -e 's|^metalink=|#metalink=|g' \ - -e 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://mirrors.tuna.tsinghua.edu.cn/fedora|g' \ - -i /etc/yum.repos.d/fedora*.repo - if [ -f /etc/yum.repos.d/fedora-cisco-openh264.repo ]; then - sed -i 's|^enabled=1|enabled=0|g' /etc/yum.repos.d/fedora-cisco-openh264.repo - fi - dnf makecache --refresh -y - dnf install -y git nodejs - - - name: Checkout - uses: actions/checkout@v4 - - - name: Install cross build dependencies - run: | - set -euo pipefail - dnf install -y \ - cmake \ - curl \ - ninja-build \ - p7zip \ - p7zip-plugins \ - python3 \ - mingw64-gcc-c++ \ - mingw64-qt6-qtbase \ - mingw64-opencv - - - name: Configure - run: | - set -euo pipefail - cmake -S . -B build-windows-cross -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_TOOLCHAIN_FILE=/usr/share/mingw/toolchain-mingw64.cmake - - - name: Build - run: | - set -euo pipefail - cmake --build build-windows-cross --parallel "$(nproc)" - - - name: Package - run: | - set -euo pipefail - APP_NAME="chengnan" - PACKAGE_NAME="chengnan-windows-x86_64-cross" - PACKAGE_DIR="dist-windows-cross/${PACKAGE_NAME}" - EXE="build-windows-cross/${APP_NAME}.exe" - - test -f "$EXE" - rm -rf dist-windows-cross - mkdir -p "$PACKAGE_DIR" - cp "$EXE" "$PACKAGE_DIR/" - - # 收集 MinGW 运行时、Qt、OpenCV 依赖 DLL。 - for round in 1 2 3; do - while IFS= read -r binary; do - while IFS= read -r dll; do - [ -f "$dll" ] || continue - cp -n "$dll" "$PACKAGE_DIR/" || true - done < <(x86_64-w64-mingw32-objdump -p "$binary" \ - | awk '/DLL Name:/ {print $3}' \ - | while read -r name; do \ - find /usr/x86_64-w64-mingw32/sys-root/mingw/bin -iname "$name" -print -quit; \ - done) - done < <(find "$PACKAGE_DIR" -maxdepth 1 -type f \( -name '*.exe' -o -name '*.dll' \) | sort) - done - - # Qt 平台插件是 Windows Qt GUI 程序启动所必需的。 - QT_PLUGIN_ROOT="/usr/x86_64-w64-mingw32/sys-root/mingw/lib/qt6/plugins" - if [ -d "$QT_PLUGIN_ROOT/platforms" ]; then - mkdir -p "$PACKAGE_DIR/platforms" - cp -n "$QT_PLUGIN_ROOT/platforms"/*.dll "$PACKAGE_DIR/platforms/" - fi - - cat > "$PACKAGE_DIR/README.txt" <<'EOF' - chengnan Windows x86_64 交叉编译发布包 - - 运行方式: - 1. 解压整个目录。 - 2. 双击 chengnan.exe 运行。 - - 说明: - - 本目录由 Linux Runner 使用 MinGW-w64 交叉编译生成。 - - 请不要只复制 chengnan.exe,需保留同目录 DLL 和 platforms 子目录。 - EOF - - (cd dist-windows-cross && 7z a -tzip "${PACKAGE_NAME}.zip" "${PACKAGE_NAME}" >/dev/null) - (cd dist-windows-cross && sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256") - - du -h "dist-windows-cross/${PACKAGE_NAME}.zip" - - - name: Verify package - run: | - set -euo pipefail - test -s dist-windows-cross/chengnan-windows-x86_64-cross.zip - test -s dist-windows-cross/chengnan-windows-x86_64-cross.zip.sha256 - (cd dist-windows-cross && sha256sum -c chengnan-windows-x86_64-cross.zip.sha256) - 7z l dist-windows-cross/chengnan-windows-x86_64-cross.zip | grep 'chengnan.exe' - 7z l dist-windows-cross/chengnan-windows-x86_64-cross.zip | grep 'platforms/' - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: chengnan-windows-x86_64-cross - path: | - dist-windows-cross/chengnan-windows-x86_64-cross.zip - dist-windows-cross/chengnan-windows-x86_64-cross.zip.sha256 - if-no-files-found: error - - - name: Upload package to repository release - if: ${{ github.event_name != 'pull_request' }} - env: - RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} - run: | - set -euo pipefail - PACKAGE_NAME="chengnan-windows-x86_64-cross" - SERVER_URL="${GITHUB_SERVER_URL:-https://git.luochen570.cn}" - REPOSITORY="${GITHUB_REPOSITORY}" - API_BASE="${SERVER_URL}/api/v1/repos/${REPOSITORY}" - TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN:-}}" - test -n "$TOKEN" - - if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then - TAG_NAME="${GITHUB_REF_NAME}" - RELEASE_NAME="${GITHUB_REF_NAME}" - PRERELEASE=false - else - TAG_NAME="build-${GITHUB_SHA:0:10}" - RELEASE_NAME="chengnan ${GITHUB_SHA:0:10}" - PRERELEASE=true - fi - - json_escape() { - python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' - } - - release_payload="$(printf '{"tag_name":%s,"target_commitish":%s,"name":%s,"body":%s,"draft":false,"prerelease":%s}' \ - "$(printf '%s' "$TAG_NAME" | json_escape)" \ - "$(printf '%s' "${GITHUB_SHA}" | json_escape)" \ - "$(printf '%s' "$RELEASE_NAME" | json_escape)" \ - "$(printf 'Automated Windows x86_64 cross build from Gitea Actions.\n' | json_escape)" \ - "$PRERELEASE")" - - status="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - --header 'Content-Type: application/json' \ - --data "$release_payload" \ - --write-out '%{http_code}' \ - --output /tmp/gitea-release-response.json \ - "${API_BASE}/releases")" - - if [ "$status" = "409" ] || [ "$status" = "422" ]; then - RELEASE_ID="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/${TAG_NAME}" \ - | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" - elif [ "$status" = "201" ] || [ "$status" = "200" ]; then - RELEASE_ID="$(python3 -c 'import json,sys; print(json.load(open("/tmp/gitea-release-response.json"))["id"])')" - else - echo "Create release failed with HTTP ${status}" >&2 - cat /tmp/gitea-release-response.json >&2 - exit 1 - fi - - upload_asset() { - local src="$1" - local name - local existing_id - local status - name="$(basename "$src")" - existing_id="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" \ - | ASSET_NAME="$name" python3 -c 'import json,os,sys; assets=json.load(sys.stdin); print(next((str(a["id"]) for a in assets if a.get("name")==os.environ["ASSET_NAME"]), ""))')" - if [ -n "$existing_id" ]; then - curl --show-error --silent --location \ - --request DELETE \ - --header "Authorization: token ${TOKEN}" \ - --output /tmp/gitea-release-asset-delete-response.json \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${existing_id}" - fi - status="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - --form "attachment=@${src};filename=${name}" \ - --write-out '%{http_code}' \ - --output /tmp/gitea-release-asset-response.json \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${name}")" - if [ "$status" != "201" ] && [ "$status" != "200" ]; then - echo "Upload release asset failed with HTTP ${status}: ${name}" >&2 - cat /tmp/gitea-release-asset-response.json >&2 - exit 1 - fi - } - - upload_asset "dist-windows-cross/${PACKAGE_NAME}.zip" - upload_asset "dist-windows-cross/${PACKAGE_NAME}.zip.sha256" - echo "Release assets uploaded to ${SERVER_URL}/${REPOSITORY}/releases/tag/${TAG_NAME}" diff --git a/.gitea/workflows/build-windows-msys2-release.yml b/.gitea/workflows/build-windows-msys2-release.yml deleted file mode 100644 index 8c8f6b1..0000000 --- a/.gitea/workflows/build-windows-msys2-release.yml +++ /dev/null @@ -1,180 +0,0 @@ -name: Build Windows Release with MSYS2 - -on: - push: - branches: - - main - tags: - - 'v*' - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - build-windows-msys2: - # 需要 Gitea act_runner 运行在 Windows,并且能访问 GitHub 以拉取 actions/checkout、msys2/setup-msys2、actions/upload-artifact。 - # 如果你的 runner 使用了自定义标签,可以在 Gitea runner 配置中给它同时加上 windows 标签。 - runs-on: - - windows - - defaults: - run: - shell: msys2 {0} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup MSYS2 build environment - uses: msys2/setup-msys2@v2 - with: - msystem: UCRT64 - update: true - install: >- - git - curl - zip - p7zip - mingw-w64-ucrt-x86_64-toolchain - mingw-w64-ucrt-x86_64-cmake - mingw-w64-ucrt-x86_64-ninja - mingw-w64-ucrt-x86_64-qt6-base - mingw-w64-ucrt-x86_64-opencv - - - name: Configure - run: | - set -euo pipefail - cmake -S . -B build-windows-msys2 -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH=/ucrt64 - - - name: Build - run: | - set -euo pipefail - cmake --build build-windows-msys2 --parallel "$(nproc)" - - - name: Package - run: | - set -euo pipefail - APP_NAME="chengnan" - PACKAGE_NAME="chengnan-windows-x86_64-msys2" - PACKAGE_DIR="dist-windows-msys2/${PACKAGE_NAME}" - EXE="build-windows-msys2/${APP_NAME}.exe" - - test -f "$EXE" - rm -rf dist-windows-msys2 - mkdir -p "$PACKAGE_DIR" - cp "$EXE" "$PACKAGE_DIR/" - - # 部署 Qt 运行时和插件。 - if command -v windeployqt >/dev/null 2>&1; then - windeployqt --release --no-translations "$PACKAGE_DIR/${APP_NAME}.exe" - elif command -v windeployqt6 >/dev/null 2>&1; then - windeployqt6 --release --no-translations "$PACKAGE_DIR/${APP_NAME}.exe" - else - echo "windeployqt not found" >&2 - exit 1 - fi - - # 收集 MinGW/MSYS2/OpenCV 运行时 DLL。ldd 会列出 exe/dll 依赖;循环几轮收集二级依赖。 - for round in 1 2 3; do - while IFS= read -r binary; do - while IFS= read -r dll; do - [ -f "$dll" ] || continue - cp -n "$dll" "$PACKAGE_DIR/" || true - done < <(ldd "$binary" \ - | awk '/=> \/ucrt64\/bin\// {print $3} /^\/ucrt64\/bin\// {print $1}' \ - | sort -u) - done < <(find "$PACKAGE_DIR" -maxdepth 2 -type f \( -name '*.exe' -o -name '*.dll' \) | sort) - done - - cat > "$PACKAGE_DIR/README.txt" <<'EOF' - chengnan Windows x86_64 MSYS2 发布包 - - 运行方式: - 1. 解压整个目录。 - 2. 双击 chengnan.exe 运行。 - - 说明: - - 本目录由 Windows Runner 使用 MSYS2 UCRT64 构建生成。 - - 本目录已包含 Qt、OpenCV 和 MinGW 运行时 DLL。 - - 请不要只复制 chengnan.exe,需保留同目录 DLL 和 Qt plugins 子目录。 - EOF - - (cd dist-windows-msys2 && 7z a -tzip "${PACKAGE_NAME}.zip" "${PACKAGE_NAME}" >/dev/null) - (cd dist-windows-msys2 && sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256") - - # 当前 Gitea 上传入口约 60 秒会返回 502;大文件按 15 MiB 分片上传。 - ZIP="dist-windows-msys2/${PACKAGE_NAME}.zip" - split -b 15M -d -a 3 "$ZIP" "${ZIP}.part-" - (cd dist-windows-msys2 && sha256sum "${PACKAGE_NAME}.zip".part-* > "${PACKAGE_NAME}.zip.parts.sha256") - du -h "$ZIP" "${ZIP}".part-* - - - name: Verify package - run: | - set -euo pipefail - test -s dist-windows-msys2/chengnan-windows-x86_64-msys2.zip - test -s dist-windows-msys2/chengnan-windows-x86_64-msys2.zip.part-000 - test -s dist-windows-msys2/chengnan-windows-x86_64-msys2.zip.sha256 - test -s dist-windows-msys2/chengnan-windows-x86_64-msys2.zip.parts.sha256 - (cd dist-windows-msys2 && sha256sum -c chengnan-windows-x86_64-msys2.zip.sha256) - (cd dist-windows-msys2 && sha256sum -c chengnan-windows-x86_64-msys2.zip.parts.sha256) - 7z l dist-windows-msys2/chengnan-windows-x86_64-msys2.zip | grep 'chengnan.exe' - limit=$((20 * 1024 * 1024)) - for part in dist-windows-msys2/chengnan-windows-x86_64-msys2.zip.part-*; do - size="$(wc -c < "$part")" - echo "$(basename "$part") $size" - if [ "$size" -gt "$limit" ]; then - echo "$part is too large for the current Gitea upload path" >&2 - exit 1 - fi - done - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: chengnan-windows-x86_64-msys2 - path: | - dist-windows-msys2/chengnan-windows-x86_64-msys2.zip - dist-windows-msys2/chengnan-windows-x86_64-msys2.zip.sha256 - dist-windows-msys2/chengnan-windows-x86_64-msys2.zip.part-* - dist-windows-msys2/chengnan-windows-x86_64-msys2.zip.parts.sha256 - if-no-files-found: error - - - name: Upload package to Gitea Packages - if: ${{ github.event_name != 'pull_request' }} - env: - PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} - run: | - set -euo pipefail - PACKAGE_NAME="chengnan-windows-x86_64-msys2" - VERSION="${GITHUB_SHA:-manual}" - VERSION="${VERSION:0:10}" - OWNER="${GITHUB_REPOSITORY%%/*}" - API_BASE="${GITHUB_SERVER_URL:-https://git.luochen570.cn}/api/packages/${OWNER}/generic/chengnan/${VERSION}" - TOKEN="${PACKAGE_TOKEN:-${GITHUB_TOKEN:-}}" - test -n "$TOKEN" - upload_file() { - local src="$1" - local url="$2" - local status - status="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - --upload-file "$src" \ - --write-out '%{http_code}' \ - --output /tmp/gitea-upload-response.txt \ - "$url")" - if [ "$status" != "201" ] && [ "$status" != "200" ]; then - echo "Upload failed with HTTP ${status}: ${url}" >&2 - cat /tmp/gitea-upload-response.txt >&2 - exit 1 - fi - } - for part in dist-windows-msys2/${PACKAGE_NAME}.zip.part-*; do - upload_file "$part" "${API_BASE}/$(basename "$part")" - done - upload_file "dist-windows-msys2/${PACKAGE_NAME}.zip.sha256" "${API_BASE}/${PACKAGE_NAME}.zip.sha256" - upload_file "dist-windows-msys2/${PACKAGE_NAME}.zip.parts.sha256" "${API_BASE}/${PACKAGE_NAME}.zip.parts.sha256" - echo "Package uploaded to: ${API_BASE}/" - echo "Download all ${PACKAGE_NAME}.zip.part-* files, then reconstruct with: cat ${PACKAGE_NAME}.zip.part-* > ${PACKAGE_NAME}.zip" diff --git a/.gitea/workflows/build-windows-release.yml b/.gitea/workflows/build-windows-release.yml deleted file mode 100644 index d7ebb18..0000000 --- a/.gitea/workflows/build-windows-release.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Build Windows Release - -on: - workflow_dispatch: - -jobs: - build-windows: - # Gitea Actions 使用自建 act_runner,不提供 GitHub 托管的 windows-latest。 - # 这里匹配常见的 Windows Runner 标签;Runner 需注册为 windows 或 windows-latest 才会接单。 - runs-on: - - windows - defaults: - run: - shell: msys2 {0} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup MSYS2 build environment - uses: msys2/setup-msys2@v2 - with: - msystem: UCRT64 - update: true - install: >- - git - zip - p7zip - mingw-w64-ucrt-x86_64-toolchain - mingw-w64-ucrt-x86_64-cmake - mingw-w64-ucrt-x86_64-ninja - mingw-w64-ucrt-x86_64-qt6-base - mingw-w64-ucrt-x86_64-opencv - - - name: Configure - run: | - cmake -S . -B build-windows -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH=/ucrt64 - - - name: Build - run: | - cmake --build build-windows --parallel "$(nproc)" - - - name: Package - run: | - set -euo pipefail - APP_NAME="chengnan" - PACKAGE_NAME="chengnan-windows-x86_64" - PACKAGE_DIR="dist-windows/${PACKAGE_NAME}" - EXE="build-windows/${APP_NAME}.exe" - - test -f "$EXE" - rm -rf dist-windows - mkdir -p "$PACKAGE_DIR" - cp "$EXE" "$PACKAGE_DIR/" - - # 部署 Qt 运行时和插件。 - if command -v windeployqt >/dev/null 2>&1; then - windeployqt --release --no-translations "$PACKAGE_DIR/${APP_NAME}.exe" - elif command -v windeployqt6 >/dev/null 2>&1; then - windeployqt6 --release --no-translations "$PACKAGE_DIR/${APP_NAME}.exe" - else - echo "windeployqt not found" >&2 - exit 1 - fi - - # 收集 MinGW/MSYS2/OpenCV 运行时 DLL。ldd 会列出 exe/dll 依赖;循环几轮收集二级依赖。 - for round in 1 2 3; do - while IFS= read -r binary; do - while IFS= read -r dll; do - [ -f "$dll" ] || continue - cp -n "$dll" "$PACKAGE_DIR/" || true - done < <(ldd "$binary" \ - | awk '/=> \/ucrt64\/bin\// {print $3} /^\/ucrt64\/bin\// {print $1}' \ - | sort -u) - done < <(find "$PACKAGE_DIR" -maxdepth 2 -type f \( -name '*.exe' -o -name '*.dll' \) | sort) - done - - cat > "$PACKAGE_DIR/README.txt" <<'EOF' - chengnan Windows x86_64 发布包 - - 运行方式: - 1. 解压整个目录。 - 2. 双击 chengnan.exe 运行。 - - 说明: - - 本目录已包含 Qt、OpenCV 和 MinGW 运行时 DLL。 - - 请不要只复制 chengnan.exe,需保留同目录 DLL 和 Qt plugins 子目录。 - EOF - - (cd dist-windows && 7z a -tzip "${PACKAGE_NAME}.zip" "${PACKAGE_NAME}" >/dev/null) - (cd dist-windows && sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256") - - - name: Verify package - run: | - test -s dist-windows/chengnan-windows-x86_64.zip - sha256sum -c dist-windows/chengnan-windows-x86_64.zip.sha256 - 7z l dist-windows/chengnan-windows-x86_64.zip | grep 'chengnan.exe' - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: chengnan-windows-x86_64 - path: | - dist-windows/chengnan-windows-x86_64.zip - dist-windows/chengnan-windows-x86_64.zip.sha256 - if-no-files-found: error diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4df3a2c..0000000 --- a/.gitignore +++ /dev/null @@ -1,52 +0,0 @@ -# 构建输出与发布产物 -/build*/ -/dist*/ -/out*/ -/release*/ -/debug*/ - -# 编译工具、SDK、第三方依赖缓存 -# 本仓库只保存源码;CMake/Ninja/Qt/OpenCV/MSYS2/vcpkg 等均由工作流临时安装。 -/tools/ -/third_party/ -/downloads/ -/.cache/ -/vcpkg_installed/ - -# CMake / Ninja / 编译数据库 -CMakeCache.txt -CMakeFiles/ -cmake_install.cmake -install_manifest.txt -.ninja_* -build.ninja -compile_commands.json - -# Windows / Qt / C++ 本地产物 -*.exe -*.dll -*.lib -*.pdb -*.ilk -*.obj -*.exp -*.manifest -*.qrc.depends -moc_*.cpp -qrc_*.cpp -ui_*.h -*_autogen/ - -# 日志和临时文件 -*.log -*.tmp -*.bak -*.swp -.DS_Store -Thumbs.db - -# IDE -.vscode/ -.idea/ -*.user -*.pro.user diff --git a/CMakeLists.txt b/CMakeLists.txt index d049ed9..136e3f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,61 +1,32 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 3.16) -project(chengnan VERSION 0.1.0 LANGUAGES CXX) +project(chengnan VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC OFF) +set(CMAKE_AUTORCC OFF) -find_package(Qt6 QUIET COMPONENTS Widgets Concurrent) -if (NOT Qt6_FOUND) - find_package(Qt5 REQUIRED COMPONENTS Widgets Concurrent) -endif() - -find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs) - -# Fedora MinGW OpenCV 4.13.0 may report an invalid include directory -# (//include/opencv4) from its OpenCVConfig.cmake. The headers are in the -# MinGW sysroot, so add a narrow cross-build fallback when CMake did not get a -# usable OpenCV include path from the package config. -if (MINGW AND (NOT OpenCV_INCLUDE_DIRS OR NOT EXISTS "${OpenCV_INCLUDE_DIRS}")) - set(_fedora_mingw_opencv_include "/usr/x86_64-w64-mingw32/sys-root/mingw/include/opencv4") - if (EXISTS "${_fedora_mingw_opencv_include}") - set(OpenCV_INCLUDE_DIRS "${_fedora_mingw_opencv_include}") - endif() -endif() - -set(PROJECT_SOURCES - app/src/main.cpp - app/src/MainWindow.cpp - app/src/ImageView.cpp - app/src/ImageConverter.cpp - app/src/ImageProcessor.cpp - app/src/ImageWorker.cpp - - app/include/MainWindow.h - app/include/ImageView.h - app/include/ImageConverter.h - app/include/ImageProcessor.h - app/include/ImageWorker.h -) - +find_package(Qt6 QUIET COMPONENTS Widgets) if (Qt6_FOUND) - qt_add_executable(chengnan ${PROJECT_SOURCES}) - target_link_libraries(chengnan PRIVATE Qt6::Widgets Qt6::Concurrent ${OpenCV_LIBS}) + qt_add_executable(chengnan + main.cpp + MainWindow.h + MainWindow.cpp + ) + target_link_libraries(chengnan PRIVATE Qt6::Widgets) else() - add_executable(chengnan ${PROJECT_SOURCES}) - target_link_libraries(chengnan PRIVATE Qt5::Widgets Qt5::Concurrent ${OpenCV_LIBS}) + find_package(Qt5 REQUIRED COMPONENTS Widgets) + add_executable(chengnan + main.cpp + MainWindow.h + MainWindow.cpp + ) + target_link_libraries(chengnan PRIVATE Qt5::Widgets) endif() -target_include_directories(chengnan PRIVATE - ${OpenCV_INCLUDE_DIRS} - ${CMAKE_CURRENT_SOURCE_DIR}/app/include -) - target_compile_definitions(chengnan PRIVATE - QT_NO_KEYWORDS CHENGNAN_APP_VERSION="${PROJECT_VERSION}" ) diff --git a/MainWindow.cpp b/MainWindow.cpp new file mode 100644 index 0000000..a8a3294 --- /dev/null +++ b/MainWindow.cpp @@ -0,0 +1,413 @@ +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +ImageLabel::ImageLabel(QWidget *parent) + : QLabel(parent) +{ + setAlignment(Qt::AlignCenter); + setBackgroundRole(QPalette::Base); + setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + setScaledContents(false); + setMinimumSize(320, 240); + setText(tr("打开一张图片开始查看")); +} + +void ImageLabel::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + Q_EMIT doubleClicked(); + } + QLabel::mouseDoubleClickEvent(event); +} + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + createActions(); + createMenus(); + createToolbar(); + createCentralWidget(); + updateActions(); + + setWindowTitle(tr("chengnan 图片查看器")); + resize(1100, 720); + statusBar()->showMessage(tr("就绪")); +} + +void MainWindow::createActions() +{ + openAction_ = new QAction(tr("打开(&O)"), this); + openAction_->setShortcut(QKeySequence::Open); + connect(openAction_, &QAction::triggered, this, &MainWindow::openImage); + + saveAction_ = new QAction(tr("保存(&S)"), this); + saveAction_->setShortcut(QKeySequence::Save); + connect(saveAction_, &QAction::triggered, this, &MainWindow::saveImage); + + saveAsAction_ = new QAction(tr("另存为(&A)"), this); + saveAsAction_->setShortcut(QKeySequence::SaveAs); + connect(saveAsAction_, &QAction::triggered, this, &MainWindow::saveImageAs); + + exitAction_ = new QAction(tr("退出(&X)"), this); + exitAction_->setShortcut(QKeySequence::Quit); + connect(exitAction_, &QAction::triggered, this, &QWidget::close); + + zoomInAction_ = new QAction(tr("放大"), this); + zoomInAction_->setShortcut(QKeySequence::ZoomIn); + connect(zoomInAction_, &QAction::triggered, this, &MainWindow::zoomIn); + + zoomOutAction_ = new QAction(tr("缩小"), this); + zoomOutAction_->setShortcut(QKeySequence::ZoomOut); + connect(zoomOutAction_, &QAction::triggered, this, &MainWindow::zoomOut); + + fitAction_ = new QAction(tr("适配窗口"), this); + fitAction_->setShortcut(Qt::CTRL | Qt::Key_F); + connect(fitAction_, &QAction::triggered, this, &MainWindow::fitToWindow); + + resetAction_ = new QAction(tr("原始大小"), this); + resetAction_->setShortcut(Qt::CTRL | Qt::Key_0); + connect(resetAction_, &QAction::triggered, this, &MainWindow::resetView); + + rotateLeftAction_ = new QAction(tr("左旋 90°"), this); + connect(rotateLeftAction_, &QAction::triggered, this, &MainWindow::rotateLeft); + + rotateRightAction_ = new QAction(tr("右旋 90°"), this); + connect(rotateRightAction_, &QAction::triggered, this, &MainWindow::rotateRight); + + flipHorizontalAction_ = new QAction(tr("水平翻转"), this); + connect(flipHorizontalAction_, &QAction::triggered, this, &MainWindow::flipHorizontal); + + flipVerticalAction_ = new QAction(tr("垂直翻转"), this); + connect(flipVerticalAction_, &QAction::triggered, this, &MainWindow::flipVertical); + + grayscaleAction_ = new QAction(tr("灰度化"), this); + connect(grayscaleAction_, &QAction::triggered, this, &MainWindow::grayscale); + + invertAction_ = new QAction(tr("反色"), this); + connect(invertAction_, &QAction::triggered, this, &MainWindow::invertColors); +} + +void MainWindow::createMenus() +{ + QMenu *fileMenu = menuBar()->addMenu(tr("文件(&F)")); + fileMenu->addAction(openAction_); + fileMenu->addAction(saveAction_); + fileMenu->addAction(saveAsAction_); + fileMenu->addSeparator(); + fileMenu->addAction(exitAction_); + + QMenu *viewMenu = menuBar()->addMenu(tr("查看(&V)")); + viewMenu->addAction(zoomInAction_); + viewMenu->addAction(zoomOutAction_); + viewMenu->addAction(fitAction_); + viewMenu->addAction(resetAction_); + + QMenu *processMenu = menuBar()->addMenu(tr("处理(&P)")); + processMenu->addAction(rotateLeftAction_); + processMenu->addAction(rotateRightAction_); + processMenu->addAction(flipHorizontalAction_); + processMenu->addAction(flipVerticalAction_); + processMenu->addSeparator(); + processMenu->addAction(grayscaleAction_); + processMenu->addAction(invertAction_); +} + +void MainWindow::createToolbar() +{ + QToolBar *toolbar = addToolBar(tr("主工具栏")); + toolbar->setMovable(false); + toolbar->addAction(openAction_); + toolbar->addAction(saveAction_); + toolbar->addSeparator(); + toolbar->addAction(zoomOutAction_); + + zoomSlider_ = new QSlider(Qt::Horizontal, this); + zoomSlider_->setRange(10, 400); + zoomSlider_->setValue(100); + zoomSlider_->setFixedWidth(180); + zoomSlider_->setToolTip(tr("缩放比例")); + connect(zoomSlider_, &QSlider::valueChanged, this, &MainWindow::updateZoomFromSlider); + toolbar->addWidget(zoomSlider_); + + toolbar->addAction(zoomInAction_); + toolbar->addAction(fitAction_); + toolbar->addSeparator(); + toolbar->addAction(rotateLeftAction_); + toolbar->addAction(rotateRightAction_); + toolbar->addAction(grayscaleAction_); + toolbar->addAction(invertAction_); +} + +void MainWindow::createCentralWidget() +{ + imageLabel_ = new ImageLabel(this); + connect(imageLabel_, &ImageLabel::doubleClicked, this, &MainWindow::fitToWindow); + + scrollArea_ = new QScrollArea(this); + scrollArea_->setBackgroundRole(QPalette::Dark); + scrollArea_->setWidget(imageLabel_); + scrollArea_->setWidgetResizable(true); + setCentralWidget(scrollArea_); +} + +void MainWindow::openImage() +{ + const QString fileName = QFileDialog::getOpenFileName( + this, + tr("打开图片"), + QString(), + tr("图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.webp);;所有文件 (*.*)")); + + if (fileName.isEmpty()) { + return; + } + + QImageReader reader(fileName); + reader.setAutoTransform(true); + const QImage image = reader.read(); + if (image.isNull()) { + QMessageBox::warning(this, tr("打开失败"), tr("无法读取图片:%1").arg(reader.errorString())); + return; + } + + setImage(image, fileName); +} + +void MainWindow::saveImage() +{ + if (!ensureImageLoaded(tr("保存"))) { + return; + } + + if (currentPath_.isEmpty()) { + saveImageAs(); + return; + } + + writeImageToPath(currentPath_); +} + +void MainWindow::saveImageAs() +{ + if (!ensureImageLoaded(tr("另存为"))) { + return; + } + + const QString fileName = QFileDialog::getSaveFileName( + this, + tr("另存为"), + currentPath_.isEmpty() ? QStringLiteral("chengnan.png") : currentPath_, + tr("PNG 图片 (*.png);;JPEG 图片 (*.jpg *.jpeg);;BMP 图片 (*.bmp);;所有文件 (*.*)")); + + if (!fileName.isEmpty()) { + writeImageToPath(fileName); + } +} + +void MainWindow::zoomIn() +{ + setZoomFactor(zoomFactor_ * 1.25); +} + +void MainWindow::zoomOut() +{ + setZoomFactor(zoomFactor_ / 1.25); +} + +void MainWindow::fitToWindow() +{ + if (!ensureImageLoaded(tr("适配窗口"))) { + return; + } + + const QSize viewportSize = scrollArea_->viewport()->size(); + const QSize imageSize = currentImage_.size(); + if (imageSize.isEmpty()) { + return; + } + + const double xRatio = static_cast(viewportSize.width()) / imageSize.width(); + const double yRatio = static_cast(viewportSize.height()) / imageSize.height(); + setZoomFactor(std::min(xRatio, yRatio)); +} + +void MainWindow::resetView() +{ + setZoomFactor(1.0); +} + +void MainWindow::rotateLeft() +{ + if (!ensureImageLoaded(tr("旋转"))) { + return; + } + QTransform transform; + transform.rotate(-90); + applyTransformedImage(currentImage_.transformed(transform, Qt::SmoothTransformation)); +} + +void MainWindow::rotateRight() +{ + if (!ensureImageLoaded(tr("旋转"))) { + return; + } + QTransform transform; + transform.rotate(90); + applyTransformedImage(currentImage_.transformed(transform, Qt::SmoothTransformation)); +} + +void MainWindow::flipHorizontal() +{ + if (!ensureImageLoaded(tr("翻转"))) { + return; + } + applyTransformedImage(currentImage_.mirrored(true, false)); +} + +void MainWindow::flipVertical() +{ + if (!ensureImageLoaded(tr("翻转"))) { + return; + } + applyTransformedImage(currentImage_.mirrored(false, true)); +} + +void MainWindow::grayscale() +{ + if (!ensureImageLoaded(tr("灰度化"))) { + return; + } + applyTransformedImage(currentImage_.convertToFormat(QImage::Format_Grayscale8)); +} + +void MainWindow::invertColors() +{ + if (!ensureImageLoaded(tr("反色"))) { + return; + } + QImage image = currentImage_.convertToFormat(QImage::Format_ARGB32); + image.invertPixels(QImage::InvertRgb); + applyTransformedImage(image); +} + +void MainWindow::updateZoomFromSlider(int value) +{ + const double factor = static_cast(value) / 100.0; + if (std::abs(factor - zoomFactor_) > 0.001) { + zoomFactor_ = factor; + updateImageView(); + } +} + +void MainWindow::setImage(const QImage &image, const QString &path) +{ + currentImage_ = image.convertToFormat(QImage::Format_ARGB32); + currentPath_ = path; + setZoomFactor(1.0); + updateActions(); + + const QFileInfo info(path); + setWindowTitle(path.isEmpty() + ? tr("chengnan 图片查看器") + : tr("%1 - chengnan 图片查看器").arg(info.fileName())); + statusBar()->showMessage(tr("已加载:%1 × %2").arg(currentImage_.width()).arg(currentImage_.height()), 5000); +} + +void MainWindow::updateImageView() +{ + if (currentImage_.isNull()) { + imageLabel_->setPixmap(QPixmap()); + imageLabel_->setText(tr("打开一张图片开始查看")); + return; + } + + const QSize scaledSize = currentImage_.size() * zoomFactor_; + const QPixmap pixmap = QPixmap::fromImage(currentImage_).scaled( + scaledSize, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + + imageLabel_->setText(QString()); + imageLabel_->setPixmap(pixmap); + imageLabel_->resize(pixmap.size()); + + if (zoomSlider_->value() != static_cast(zoomFactor_ * 100.0)) { + zoomSlider_->blockSignals(true); + zoomSlider_->setValue(static_cast(zoomFactor_ * 100.0)); + zoomSlider_->blockSignals(false); + } + + statusBar()->showMessage(tr("缩放:%1%").arg(static_cast(zoomFactor_ * 100.0))); +} + +void MainWindow::setZoomFactor(double factor) +{ + zoomFactor_ = std::clamp(factor, 0.10, 4.00); + updateImageView(); +} + +void MainWindow::applyTransformedImage(const QImage &image) +{ + currentImage_ = image.convertToFormat(QImage::Format_ARGB32); + updateImageView(); + updateActions(); + statusBar()->showMessage(tr("处理完成,可保存结果"), 3000); +} + +void MainWindow::updateActions() +{ + const bool hasImage = !currentImage_.isNull(); + saveAction_->setEnabled(hasImage); + saveAsAction_->setEnabled(hasImage); + zoomInAction_->setEnabled(hasImage); + zoomOutAction_->setEnabled(hasImage); + fitAction_->setEnabled(hasImage); + resetAction_->setEnabled(hasImage); + rotateLeftAction_->setEnabled(hasImage); + rotateRightAction_->setEnabled(hasImage); + flipHorizontalAction_->setEnabled(hasImage); + flipVerticalAction_->setEnabled(hasImage); + grayscaleAction_->setEnabled(hasImage); + invertAction_->setEnabled(hasImage); + zoomSlider_->setEnabled(hasImage); +} + +bool MainWindow::ensureImageLoaded(const QString &actionName) const +{ + if (!currentImage_.isNull()) { + return true; + } + + QMessageBox::information(nullptr, actionName, tr("请先打开一张图片。")); + return false; +} + +bool MainWindow::writeImageToPath(const QString &path) +{ + QImageWriter writer(path); + if (!writer.write(currentImage_)) { + QMessageBox::warning(this, tr("保存失败"), tr("无法保存图片:%1").arg(writer.errorString())); + return false; + } + + currentPath_ = path; + setWindowTitle(tr("%1 - chengnan 图片查看器").arg(QFileInfo(path).fileName())); + statusBar()->showMessage(tr("已保存:%1").arg(path), 5000); + return true; +} diff --git a/MainWindow.h b/MainWindow.h new file mode 100644 index 0000000..3064e9a --- /dev/null +++ b/MainWindow.h @@ -0,0 +1,90 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include + +class QAction; +class QMenu; +class QPushButton; +class QSlider; + +class ImageLabel final : public QLabel +{ + Q_OBJECT + +public: + explicit ImageLabel(QWidget *parent = nullptr); + +Q_SIGNALS: + void doubleClicked(); + +protected: + void mouseDoubleClickEvent(QMouseEvent *event) override; +}; + +class MainWindow final : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + +private Q_SLOTS: + void openImage(); + void saveImage(); + void saveImageAs(); + void zoomIn(); + void zoomOut(); + void fitToWindow(); + void resetView(); + void rotateLeft(); + void rotateRight(); + void flipHorizontal(); + void flipVertical(); + void grayscale(); + void invertColors(); + void updateZoomFromSlider(int value); + +private: + void createActions(); + void createMenus(); + void createToolbar(); + void createCentralWidget(); + void setImage(const QImage &image, const QString &path = QString()); + void updateImageView(); + void setZoomFactor(double factor); + void applyTransformedImage(const QImage &image); + void updateActions(); + bool ensureImageLoaded(const QString &actionName) const; + bool writeImageToPath(const QString &path); + + ImageLabel *imageLabel_ = nullptr; + QScrollArea *scrollArea_ = nullptr; + QSlider *zoomSlider_ = nullptr; + + QImage currentImage_; + QString currentPath_; + double zoomFactor_ = 1.0; + + QAction *openAction_ = nullptr; + QAction *saveAction_ = nullptr; + QAction *saveAsAction_ = nullptr; + QAction *exitAction_ = nullptr; + QAction *zoomInAction_ = nullptr; + QAction *zoomOutAction_ = nullptr; + QAction *fitAction_ = nullptr; + QAction *resetAction_ = nullptr; + QAction *rotateLeftAction_ = nullptr; + QAction *rotateRightAction_ = nullptr; + QAction *flipHorizontalAction_ = nullptr; + QAction *flipVerticalAction_ = nullptr; + QAction *grayscaleAction_ = nullptr; + QAction *invertAction_ = nullptr; +}; + +#endif // MAINWINDOW_H diff --git a/README.md b/README.md deleted file mode 100644 index 6dc0515..0000000 --- a/README.md +++ /dev/null @@ -1,135 +0,0 @@ -# chengnan - -chengnan 是一个面向 Windows 的图片查看器 / 简单图像处理应用,使用 **Qt Widgets + OpenCV + CMake + C++17** 开发。 - -## 项目定位 - -- 目标平台:Windows x64 -- 应用类型:桌面图片查看器 -- 构建方式:只在 Gitea Actions 工作流中构建和打包 -- 仓库内容:只保留源码、CMake 配置、文档和工作流 -- 仓库不包含:CMake/Ninja/Qt/OpenCV/MSYS2/vcpkg 等编译工具、SDK、第三方二进制依赖、本地构建产物 - -## 功能 - -当前已实现: - -- 打开图片 -- 保存图片、另存图片 -- 使用 Qt QWidget 体系显示图片,不使用 `cv::imshow` -- 左右对比显示原图和处理后图片 -- 图片视图支持: - - 鼠标滚轮缩放 - - 双击适配窗口 - - 拖拽平移视图 -- 基础图像处理: - - 放大 / 缩小 - - 平移 - - 指定角度旋转 - - 阈值分割 + 轮廓目标提取 - - 基于 HSV 均值的简单规则分类 -- 使用 `QThread` + 信号槽执行 OpenCV 处理任务,避免阻塞 UI - -## 技术栈 - -- C++17 -- Qt Widgets / Qt Concurrent -- OpenCV -- CMake -- Gitea Actions - -## 目录结构 - -```text -chengnan/ -├── .gitea/workflows/ -│ ├── build-windows-cross-linux.yml # Linux Runner 交叉编译 Windows 发布包 -│ └── build-windows-release.yml # Windows Runner 原生构建备用工作流 -├── app/ -│ ├── include/ # 头文件 -│ └── src/ # 源文件 -├── CMakeLists.txt -├── .gitignore -└── README.md -``` - -## 构建与发布 - -本项目不在仓库中保存编译工具或第三方二进制依赖。所有构建工作都在 Gitea Actions 工作流中完成。 - -当前优先使用 `.gitea/workflows/build-windows-cross-linux.yml`:在自建 `9950x` Linux Runner 的 Fedora 41 容器中,通过 MinGW-w64 交叉编译生成 Windows x86_64 发布包。`.gitea/workflows/build-windows-release.yml` 保留为 Windows Runner 原生构建备用工作流。 - -交叉编译工作流会临时完成以下步骤: - -1. 拉取源码 -2. 安装 Fedora / MinGW-w64 构建环境和依赖 -3. 配置 CMake -4. 编译 Release 版本 -5. 收集 Qt / OpenCV / MinGW 运行时依赖 -6. 打包 Windows 发布包 -7. 生成 SHA256 校验文件 -8. 上传发布包到 Gitea Packages - -## 编译工具链版本 - -交叉编译工作流固定使用以下环境和包版本: - -| 组件 | 版本 | -| --- | --- | -| Runner 标签 | `9950x` | -| 容器镜像 | `fedora:41` | -| C++ 语言标准 | `C++17` | -| MinGW-w64 GCC C++ | `mingw64-gcc-c++ 14.2.1-3.fc41` | -| CMake | `cmake 3.28.3-7.fc41` | -| Ninja | `ninja-build 1.12.1-3.fc41` | -| Qt 6 Base | `mingw64-qt6-qtbase 6.7.2-3.fc41` | -| OpenCV | `mingw64-opencv 4.10.0-1.fc41` | - -版本来源为 Fedora 41 `Everything/x86_64` 仓库元数据;构建时由工作流临时安装,不提交到源码仓库。 - -触发方式: - -- push 到 `main` -- 创建 `v*` 标签 -- Pull Request 到 `main` -- 手动触发 `workflow_dispatch` - -构建产物: - -```text -chengnan-windows-x86_64-cross.zip -chengnan-windows-x86_64-cross.zip.sha256 -``` - -发布包解压后,双击 `chengnan.exe` 运行。请保留同目录 DLL 和 Qt 插件目录,不要只复制单个 exe。 - -## 本地开发说明 - -仓库不提供本地依赖安装脚本,也不保存编译工具。若需要本地开发,请自行在 Windows 上安装: - -- CMake -- Ninja -- Qt 6 -- OpenCV -- C++ 编译器(MSVC 或 MinGW) - -然后按常规 CMake 项目方式配置和构建。正式发布以 Gitea Actions 产物为准。 - -## 依赖管理原则 - -- 不提交编译器、CMake、Ninja、Qt、OpenCV、MSYS2、vcpkg 等工具或 SDK -- 不提交 `build-*`、`dist-*`、`.exe`、`.dll` 等构建产物 -- 第三方依赖只在工作流运行期间临时安装 -- 发布包只作为工作流产物或 Gitea Packages 文件产出,不进入源码仓库 - -## 验收标准 - -- 可以打开常见图片格式,例如 PNG、JPG、BMP -- 可以保存当前处理后的图片 -- 可以另存为指定路径 -- 可以在同一界面中对比显示处理前和处理后图片 -- 支持缩放、平移、旋转操作 -- 至少实现目标提取或分类功能之一 -- 图片显示使用 QWidget 体系,不使用 OpenCV 窗口显示函数 -- 图像处理任务不会明显阻塞 UI -- Windows 发布包可直接解压运行 diff --git a/app/include/ImageConverter.h b/app/include/ImageConverter.h deleted file mode 100644 index 2f3001c..0000000 --- a/app/include/ImageConverter.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include -#include - -class ImageConverter final -{ -public: - static QImage matToQImage(const cv::Mat &mat); - static cv::Mat qImageToMat(const QImage &image); -}; diff --git a/app/include/ImageProcessor.h b/app/include/ImageProcessor.h deleted file mode 100644 index be19dd4..0000000 --- a/app/include/ImageProcessor.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include -#include -#include - -struct ProcessResult -{ - cv::Mat image; - QString description; -}; - -class ImageProcessor final -{ -public: - static ProcessResult rotate(const cv::Mat &source, double angleDegrees); - static ProcessResult scale(const cv::Mat &source, double factor); - static ProcessResult translate(const cv::Mat &source, int dx, int dy); - static ProcessResult extractTargets(const cv::Mat &source); - static ProcessResult classifySimple(const cv::Mat &source); -}; - -Q_DECLARE_METATYPE(ProcessResult) diff --git a/app/include/ImageView.h b/app/include/ImageView.h deleted file mode 100644 index 41e4fe5..0000000 --- a/app/include/ImageView.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include - -class QGraphicsPixmapItem; -class QGraphicsScene; -class QWheelEvent; -class QMouseEvent; - -class ImageView final : public QGraphicsView -{ - Q_OBJECT - -public: - explicit ImageView(QWidget *parent = nullptr); - - void setImage(const QImage &image); - void clearImage(); - void fitImageToView(); - bool hasImage() const; - -protected: - void wheelEvent(QWheelEvent *event) override; - void mouseDoubleClickEvent(QMouseEvent *event) override; - -private: - void resetSceneRect(); - - QGraphicsScene *scene_ = nullptr; - QGraphicsPixmapItem *pixmapItem_ = nullptr; - QPixmap pixmap_; - double zoomFactor_ = 1.0; -}; diff --git a/app/include/ImageWorker.h b/app/include/ImageWorker.h deleted file mode 100644 index 3f8de2c..0000000 --- a/app/include/ImageWorker.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "ImageProcessor.h" - -#include -#include -#include - -Q_DECLARE_METATYPE(cv::Mat) - -class ImageWorker final : public QObject -{ - Q_OBJECT - -public: - enum class Operation { - Rotate, - Scale, - Translate, - ExtractTargets, - ClassifySimple - }; - Q_ENUM(Operation) - -public Q_SLOTS: - void process(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy); - -Q_SIGNALS: - void finished(ProcessResult result); - void failed(QString message); -}; - -Q_DECLARE_METATYPE(ImageWorker::Operation) diff --git a/app/include/MainWindow.h b/app/include/MainWindow.h deleted file mode 100644 index 4389efe..0000000 --- a/app/include/MainWindow.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "ImageWorker.h" - -#include -#include -#include - -class QAction; -class QLabel; -class QSpinBox; -class QDoubleSpinBox; -class QPushButton; -class ImageView; - -class MainWindow final : public QMainWindow -{ - Q_OBJECT - -public: - explicit MainWindow(QWidget *parent = nullptr); - ~MainWindow() override; - -Q_SIGNALS: - void processRequested(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy); - -private Q_SLOTS: - void openImage(); - void saveImage(); - void saveImageAs(); - void resetToOriginal(); - void onProcessFinished(ProcessResult result); - void onProcessFailed(const QString &message); - void rotateLeft(); - void rotateRight(); - void scaleUp(); - void scaleDown(); - void translateUp(); - void translateDown(); - void translateLeft(); - void translateRight(); - void extractTargets(); - void classifyImage(); - -private: - void buildUi(); - void setupWorkerThread(); - void updateViews(); - void setBusy(bool busy); - bool ensureHasImage() const; - void requestProcess(ImageWorker::Operation operation, double value = 0.0, int dx = 0, int dy = 0); - bool writeCurrentImageTo(const QString &path); - - ImageView *originalView_ = nullptr; - ImageView *processedView_ = nullptr; - QLabel *infoLabel_ = nullptr; - QLabel *statusLabel_ = nullptr; - QDoubleSpinBox *scaleSpin_ = nullptr; - QDoubleSpinBox *angleSpin_ = nullptr; - QSpinBox *translateSpin_ = nullptr; - - QAction *saveAction_ = nullptr; - QAction *saveAsAction_ = nullptr; - - QThread workerThread_; - ImageWorker *worker_ = nullptr; - - cv::Mat originalImage_; - cv::Mat processedImage_; - QString currentPath_; - bool busy_ = false; -}; diff --git a/app/src/ImageConverter.cpp b/app/src/ImageConverter.cpp deleted file mode 100644 index ac188a7..0000000 --- a/app/src/ImageConverter.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "ImageConverter.h" - -#include - -QImage ImageConverter::matToQImage(const cv::Mat &mat) -{ - if (mat.empty()) { - return {}; - } - - switch (mat.type()) { - case CV_8UC1: { - QImage image(mat.data, mat.cols, mat.rows, static_cast(mat.step), QImage::Format_Grayscale8); - return image.copy(); - } - case CV_8UC3: { - cv::Mat rgb; - cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); - QImage image(rgb.data, rgb.cols, rgb.rows, static_cast(rgb.step), QImage::Format_RGB888); - return image.copy(); - } - case CV_8UC4: { - QImage image(mat.data, mat.cols, mat.rows, static_cast(mat.step), QImage::Format_ARGB32); - return image.copy(); - } - default: { - cv::Mat converted; - mat.convertTo(converted, CV_8U); - return matToQImage(converted); - } - } -} - -cv::Mat ImageConverter::qImageToMat(const QImage &image) -{ - if (image.isNull()) { - return {}; - } - - const QImage converted = image.convertToFormat(QImage::Format_RGB888); - cv::Mat rgb(converted.height(), converted.width(), CV_8UC3, - const_cast(converted.bits()), - static_cast(converted.bytesPerLine())); - cv::Mat bgr; - cv::cvtColor(rgb, bgr, cv::COLOR_RGB2BGR); - return bgr.clone(); -} diff --git a/app/src/ImageProcessor.cpp b/app/src/ImageProcessor.cpp deleted file mode 100644 index ec71952..0000000 --- a/app/src/ImageProcessor.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "ImageProcessor.h" - -#include -#include - -ProcessResult ImageProcessor::rotate(const cv::Mat &source, double angleDegrees) -{ - if (source.empty()) { - return {}; - } - - const cv::Point2f center(static_cast(source.cols) / 2.0F, static_cast(source.rows) / 2.0F); - cv::Mat matrix = cv::getRotationMatrix2D(center, angleDegrees, 1.0); - - const double absCos = std::abs(matrix.at(0, 0)); - const double absSin = std::abs(matrix.at(0, 1)); - const int boundW = static_cast(source.rows * absSin + source.cols * absCos); - const int boundH = static_cast(source.rows * absCos + source.cols * absSin); - - matrix.at(0, 2) += boundW / 2.0 - center.x; - matrix.at(1, 2) += boundH / 2.0 - center.y; - - cv::Mat output; - cv::warpAffine(source, output, matrix, cv::Size(boundW, boundH), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30)); - return {output, QStringLiteral("旋转 %1° 完成").arg(angleDegrees)}; -} - -ProcessResult ImageProcessor::scale(const cv::Mat &source, double factor) -{ - if (source.empty() || factor <= 0.0) { - return {}; - } - - cv::Mat output; - cv::resize(source, output, cv::Size(), factor, factor, factor >= 1.0 ? cv::INTER_CUBIC : cv::INTER_AREA); - return {output, QStringLiteral("缩放 %1 倍完成,尺寸:%2 × %3").arg(factor).arg(output.cols).arg(output.rows)}; -} - -ProcessResult ImageProcessor::translate(const cv::Mat &source, int dx, int dy) -{ - if (source.empty()) { - return {}; - } - - const cv::Mat matrix = (cv::Mat_(2, 3) << 1, 0, dx, 0, 1, dy); - cv::Mat output; - cv::warpAffine(source, output, matrix, source.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30)); - return {output, QStringLiteral("平移完成:dx=%1, dy=%2").arg(dx).arg(dy)}; -} - -ProcessResult ImageProcessor::extractTargets(const cv::Mat &source) -{ - if (source.empty()) { - return {}; - } - - cv::Mat gray; - if (source.channels() == 1) { - gray = source.clone(); - } else { - cv::cvtColor(source, gray, cv::COLOR_BGR2GRAY); - } - - cv::Mat blurred; - cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); - - cv::Mat binary; - cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); - - std::vector> contours; - cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); - - cv::Mat output; - if (source.channels() == 1) { - cv::cvtColor(source, output, cv::COLOR_GRAY2BGR); - } else { - output = source.clone(); - } - - int validCount = 0; - double totalArea = 0.0; - const double minArea = std::max(80.0, source.cols * source.rows * 0.0005); - - for (const auto &contour : contours) { - const double area = cv::contourArea(contour); - if (area < minArea) { - continue; - } - ++validCount; - totalArea += area; - const cv::Rect box = cv::boundingRect(contour); - cv::rectangle(output, box, cv::Scalar(0, 0, 255), 2); - cv::drawContours(output, std::vector>{contour}, -1, cv::Scalar(0, 255, 0), 2); - const cv::Moments moments = cv::moments(contour); - if (moments.m00 != 0.0) { - const int cx = static_cast(moments.m10 / moments.m00); - const int cy = static_cast(moments.m01 / moments.m00); - cv::circle(output, cv::Point(cx, cy), 4, cv::Scalar(255, 0, 0), -1); - } - } - - return {output, QStringLiteral("目标提取完成:检测到 %1 个目标,总面积约 %2 像素") - .arg(validCount) - .arg(static_cast(totalArea))}; -} - -ProcessResult ImageProcessor::classifySimple(const cv::Mat &source) -{ - if (source.empty()) { - return {}; - } - - cv::Mat hsv; - if (source.channels() == 1) { - cv::cvtColor(source, hsv, cv::COLOR_GRAY2BGR); - cv::cvtColor(hsv, hsv, cv::COLOR_BGR2HSV); - } else { - cv::cvtColor(source, hsv, cv::COLOR_BGR2HSV); - } - - const cv::Scalar meanHsv = cv::mean(hsv); - const double hue = meanHsv[0]; - const double saturation = meanHsv[1]; - const double value = meanHsv[2]; - - QString type; - if (value < 70) { - type = QStringLiteral("偏暗图像"); - } else if (saturation < 45) { - type = QStringLiteral("低饱和 / 灰度感图像"); - } else if (hue < 15 || hue > 160) { - type = QStringLiteral("红色系图像"); - } else if (hue < 45) { - type = QStringLiteral("黄色 / 橙色系图像"); - } else if (hue < 85) { - type = QStringLiteral("绿色系图像"); - } else if (hue < 130) { - type = QStringLiteral("蓝色系图像"); - } else { - type = QStringLiteral("紫色系图像"); - } - - cv::Mat output = source.clone(); - return {output, QStringLiteral("简单分类:%1(平均 H=%2, S=%3, V=%4)") - .arg(type) - .arg(hue, 0, 'f', 1) - .arg(saturation, 0, 'f', 1) - .arg(value, 0, 'f', 1)}; -} diff --git a/app/src/ImageView.cpp b/app/src/ImageView.cpp deleted file mode 100644 index 2f221c1..0000000 --- a/app/src/ImageView.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "ImageView.h" - -#include -#include -#include -#include -#include -#include - -ImageView::ImageView(QWidget *parent) - : QGraphicsView(parent), scene_(new QGraphicsScene(this)) -{ - setScene(scene_); - pixmapItem_ = scene_->addPixmap(QPixmap()); - pixmapItem_->setTransformationMode(Qt::SmoothTransformation); - - setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); - setDragMode(QGraphicsView::ScrollHandDrag); - setViewportUpdateMode(QGraphicsView::SmartViewportUpdate); - setBackgroundBrush(QColor(36, 36, 36)); - setFrameShape(QFrame::StyledPanel); - setAlignment(Qt::AlignCenter); -} - -void ImageView::setImage(const QImage &image) -{ - if (image.isNull()) { - clearImage(); - return; - } - - pixmap_ = QPixmap::fromImage(image); - pixmapItem_->setPixmap(pixmap_); - resetSceneRect(); - fitImageToView(); -} - -void ImageView::clearImage() -{ - pixmap_ = QPixmap(); - pixmapItem_->setPixmap(QPixmap()); - scene_->setSceneRect(QRectF()); - resetTransform(); - zoomFactor_ = 1.0; -} - -void ImageView::fitImageToView() -{ - if (!hasImage()) { - return; - } - resetTransform(); - fitInView(pixmapItem_, Qt::KeepAspectRatio); - zoomFactor_ = transform().m11(); -} - -bool ImageView::hasImage() const -{ - return !pixmap_.isNull(); -} - -void ImageView::wheelEvent(QWheelEvent *event) -{ - if (!hasImage()) { - QGraphicsView::wheelEvent(event); - return; - } - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - const int delta = event->angleDelta().y(); -#else - const int delta = event->delta(); -#endif - const double factor = delta > 0 ? 1.15 : 1.0 / 1.15; - zoomFactor_ *= factor; - scale(factor, factor); - event->accept(); -} - -void ImageView::mouseDoubleClickEvent(QMouseEvent *event) -{ - Q_UNUSED(event) - fitImageToView(); -} - -void ImageView::resetSceneRect() -{ - scene_->setSceneRect(pixmap_.rect()); -} diff --git a/app/src/ImageWorker.cpp b/app/src/ImageWorker.cpp deleted file mode 100644 index dfd2992..0000000 --- a/app/src/ImageWorker.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "ImageWorker.h" - -void ImageWorker::process(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy) -{ - if (image.empty()) { - Q_EMIT failed(QStringLiteral("没有可处理的图片")); - return; - } - - ProcessResult result; - switch (operation) { - case Operation::Rotate: - result = ImageProcessor::rotate(image, value); - break; - case Operation::Scale: - result = ImageProcessor::scale(image, value); - break; - case Operation::Translate: - result = ImageProcessor::translate(image, dx, dy); - break; - case Operation::ExtractTargets: - result = ImageProcessor::extractTargets(image); - break; - case Operation::ClassifySimple: - result = ImageProcessor::classifySimple(image); - break; - } - - if (result.image.empty()) { - Q_EMIT failed(QStringLiteral("图像处理失败")); - return; - } - - Q_EMIT finished(result); -} diff --git a/app/src/MainWindow.cpp b/app/src/MainWindow.cpp deleted file mode 100644 index 27ac3ad..0000000 --- a/app/src/MainWindow.cpp +++ /dev/null @@ -1,369 +0,0 @@ -#include "MainWindow.h" - -#include "ImageConverter.h" -#include "ImageView.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent) -{ - qRegisterMetaType("ProcessResult"); - qRegisterMetaType("ImageWorker::Operation"); - qRegisterMetaType("cv::Mat"); - - buildUi(); - setupWorkerThread(); - setWindowTitle(QStringLiteral("chengnan - Qt/OpenCV 图像处理")); -} - -MainWindow::~MainWindow() -{ - workerThread_.quit(); - workerThread_.wait(); -} - -void MainWindow::buildUi() -{ - auto *openAction = new QAction(QStringLiteral("打开图片"), this); - saveAction_ = new QAction(QStringLiteral("保存图片"), this); - saveAsAction_ = new QAction(QStringLiteral("另存图片"), this); - auto *resetAction = new QAction(QStringLiteral("还原原图"), this); - auto *exitAction = new QAction(QStringLiteral("退出"), this); - - connect(openAction, &QAction::triggered, this, &MainWindow::openImage); - connect(saveAction_, &QAction::triggered, this, &MainWindow::saveImage); - connect(saveAsAction_, &QAction::triggered, this, &MainWindow::saveImageAs); - connect(resetAction, &QAction::triggered, this, &MainWindow::resetToOriginal); - connect(exitAction, &QAction::triggered, qApp, &QApplication::quit); - - auto *fileMenu = menuBar()->addMenu(QStringLiteral("文件")); - fileMenu->addAction(openAction); - fileMenu->addAction(saveAction_); - fileMenu->addAction(saveAsAction_); - fileMenu->addSeparator(); - fileMenu->addAction(exitAction); - - auto *editMenu = menuBar()->addMenu(QStringLiteral("处理")); - editMenu->addAction(resetAction); - - auto *toolbar = addToolBar(QStringLiteral("主工具栏")); - toolbar->addAction(openAction); - toolbar->addAction(saveAction_); - toolbar->addAction(saveAsAction_); - toolbar->addAction(resetAction); - - originalView_ = new ImageView(this); - processedView_ = new ImageView(this); - - auto *leftBox = new QGroupBox(QStringLiteral("处理前图像"), this); - auto *leftLayout = new QVBoxLayout(leftBox); - leftLayout->addWidget(originalView_); - - auto *rightBox = new QGroupBox(QStringLiteral("处理后图像"), this); - auto *rightLayout = new QVBoxLayout(rightBox); - rightLayout->addWidget(processedView_); - - auto *splitter = new QSplitter(Qt::Horizontal, this); - splitter->addWidget(leftBox); - splitter->addWidget(rightBox); - splitter->setStretchFactor(0, 1); - splitter->setStretchFactor(1, 1); - - scaleSpin_ = new QDoubleSpinBox(this); - scaleSpin_->setRange(0.1, 5.0); - scaleSpin_->setSingleStep(0.1); - scaleSpin_->setValue(1.25); - scaleSpin_->setSuffix(QStringLiteral(" 倍")); - - angleSpin_ = new QDoubleSpinBox(this); - angleSpin_->setRange(-360.0, 360.0); - angleSpin_->setSingleStep(5.0); - angleSpin_->setValue(90.0); - angleSpin_->setSuffix(QStringLiteral("°")); - - translateSpin_ = new QSpinBox(this); - translateSpin_->setRange(1, 1000); - translateSpin_->setValue(40); - translateSpin_->setSuffix(QStringLiteral(" px")); - - auto *scaleUpButton = new QPushButton(QStringLiteral("放大"), this); - auto *scaleDownButton = new QPushButton(QStringLiteral("缩小"), this); - auto *rotateLeftButton = new QPushButton(QStringLiteral("逆时针旋转"), this); - auto *rotateRightButton = new QPushButton(QStringLiteral("顺时针旋转"), this); - auto *upButton = new QPushButton(QStringLiteral("上移"), this); - auto *downButton = new QPushButton(QStringLiteral("下移"), this); - auto *leftButton = new QPushButton(QStringLiteral("左移"), this); - auto *rightButton = new QPushButton(QStringLiteral("右移"), this); - auto *extractButton = new QPushButton(QStringLiteral("目标提取"), this); - auto *classifyButton = new QPushButton(QStringLiteral("简单分类"), this); - - connect(scaleUpButton, &QPushButton::clicked, this, &MainWindow::scaleUp); - connect(scaleDownButton, &QPushButton::clicked, this, &MainWindow::scaleDown); - connect(rotateLeftButton, &QPushButton::clicked, this, &MainWindow::rotateLeft); - connect(rotateRightButton, &QPushButton::clicked, this, &MainWindow::rotateRight); - connect(upButton, &QPushButton::clicked, this, &MainWindow::translateUp); - connect(downButton, &QPushButton::clicked, this, &MainWindow::translateDown); - connect(leftButton, &QPushButton::clicked, this, &MainWindow::translateLeft); - connect(rightButton, &QPushButton::clicked, this, &MainWindow::translateRight); - connect(extractButton, &QPushButton::clicked, this, &MainWindow::extractTargets); - connect(classifyButton, &QPushButton::clicked, this, &MainWindow::classifyImage); - - auto *controlBox = new QGroupBox(QStringLiteral("图像处理操作"), this); - auto *controlLayout = new QHBoxLayout(controlBox); - controlLayout->addWidget(new QLabel(QStringLiteral("缩放比例:"), this)); - controlLayout->addWidget(scaleSpin_); - controlLayout->addWidget(scaleUpButton); - controlLayout->addWidget(scaleDownButton); - controlLayout->addSpacing(12); - controlLayout->addWidget(new QLabel(QStringLiteral("旋转角度:"), this)); - controlLayout->addWidget(angleSpin_); - controlLayout->addWidget(rotateLeftButton); - controlLayout->addWidget(rotateRightButton); - controlLayout->addSpacing(12); - controlLayout->addWidget(new QLabel(QStringLiteral("平移距离:"), this)); - controlLayout->addWidget(translateSpin_); - controlLayout->addWidget(upButton); - controlLayout->addWidget(downButton); - controlLayout->addWidget(leftButton); - controlLayout->addWidget(rightButton); - controlLayout->addSpacing(12); - controlLayout->addWidget(extractButton); - controlLayout->addWidget(classifyButton); - controlLayout->addStretch(1); - - infoLabel_ = new QLabel(QStringLiteral("请打开一张图片开始处理。提示:鼠标滚轮可缩放视图,双击图片可适配窗口。"), this); - infoLabel_->setWordWrap(true); - - auto *central = new QWidget(this); - auto *mainLayout = new QVBoxLayout(central); - mainLayout->addWidget(splitter, 1); - mainLayout->addWidget(controlBox); - mainLayout->addWidget(infoLabel_); - setCentralWidget(central); - - statusLabel_ = new QLabel(QStringLiteral("就绪"), this); - statusBar()->addPermanentWidget(statusLabel_, 1); - - setBusy(false); -} - -void MainWindow::setupWorkerThread() -{ - worker_ = new ImageWorker(); - worker_->moveToThread(&workerThread_); - - connect(&workerThread_, &QThread::finished, worker_, &QObject::deleteLater); - connect(this, &MainWindow::processRequested, worker_, &ImageWorker::process, Qt::QueuedConnection); - connect(worker_, &ImageWorker::finished, this, &MainWindow::onProcessFinished, Qt::QueuedConnection); - connect(worker_, &ImageWorker::failed, this, &MainWindow::onProcessFailed, Qt::QueuedConnection); - - workerThread_.start(); -} - -void MainWindow::openImage() -{ - const QString path = QFileDialog::getOpenFileName( - this, - QStringLiteral("打开图片"), - QString(), - QStringLiteral("图片文件 (*.png *.jpg *.jpeg *.bmp *.tif *.tiff);;所有文件 (*.*)")); - if (path.isEmpty()) { - return; - } - - const std::string localPath = QFile::encodeName(path).toStdString(); - cv::Mat image = cv::imread(localPath, cv::IMREAD_COLOR); - if (image.empty()) { - QMessageBox::warning(this, QStringLiteral("打开失败"), QStringLiteral("无法读取图片:%1").arg(path)); - return; - } - - originalImage_ = image; - processedImage_ = image.clone(); - currentPath_ = path; - updateViews(); - infoLabel_->setText(QStringLiteral("已打开:%1,尺寸:%2 × %3") - .arg(QFileInfo(path).fileName()) - .arg(image.cols) - .arg(image.rows)); - statusLabel_->setText(QStringLiteral("已打开图片")); -} - -void MainWindow::saveImage() -{ - if (!ensureHasImage()) { - return; - } - if (currentPath_.isEmpty()) { - saveImageAs(); - return; - } - writeCurrentImageTo(currentPath_); -} - -void MainWindow::saveImageAs() -{ - if (!ensureHasImage()) { - return; - } - - const QString path = QFileDialog::getSaveFileName( - this, - QStringLiteral("另存图片"), - currentPath_.isEmpty() ? QStringLiteral("processed.png") : currentPath_, - QStringLiteral("PNG 图片 (*.png);;JPEG 图片 (*.jpg *.jpeg);;BMP 图片 (*.bmp);;所有文件 (*.*)")); - if (path.isEmpty()) { - return; - } - if (writeCurrentImageTo(path)) { - currentPath_ = path; - } -} - -void MainWindow::resetToOriginal() -{ - if (!ensureHasImage()) { - return; - } - processedImage_ = originalImage_.clone(); - updateViews(); - infoLabel_->setText(QStringLiteral("已还原为原始图片")); -} - -void MainWindow::onProcessFinished(ProcessResult result) -{ - processedImage_ = result.image; - updateViews(); - infoLabel_->setText(result.description); - statusLabel_->setText(QStringLiteral("处理完成")); - setBusy(false); -} - -void MainWindow::onProcessFailed(const QString &message) -{ - QMessageBox::warning(this, QStringLiteral("处理失败"), message); - statusLabel_->setText(QStringLiteral("处理失败")); - setBusy(false); -} - -void MainWindow::rotateLeft() -{ - requestProcess(ImageWorker::Operation::Rotate, -angleSpin_->value()); -} - -void MainWindow::rotateRight() -{ - requestProcess(ImageWorker::Operation::Rotate, angleSpin_->value()); -} - -void MainWindow::scaleUp() -{ - requestProcess(ImageWorker::Operation::Scale, scaleSpin_->value()); -} - -void MainWindow::scaleDown() -{ - requestProcess(ImageWorker::Operation::Scale, 1.0 / scaleSpin_->value()); -} - -void MainWindow::translateUp() -{ - requestProcess(ImageWorker::Operation::Translate, 0.0, 0, -translateSpin_->value()); -} - -void MainWindow::translateDown() -{ - requestProcess(ImageWorker::Operation::Translate, 0.0, 0, translateSpin_->value()); -} - -void MainWindow::translateLeft() -{ - requestProcess(ImageWorker::Operation::Translate, 0.0, -translateSpin_->value(), 0); -} - -void MainWindow::translateRight() -{ - requestProcess(ImageWorker::Operation::Translate, 0.0, translateSpin_->value(), 0); -} - -void MainWindow::extractTargets() -{ - requestProcess(ImageWorker::Operation::ExtractTargets); -} - -void MainWindow::classifyImage() -{ - requestProcess(ImageWorker::Operation::ClassifySimple); -} - -void MainWindow::updateViews() -{ - originalView_->setImage(ImageConverter::matToQImage(originalImage_)); - processedView_->setImage(ImageConverter::matToQImage(processedImage_)); -} - -void MainWindow::setBusy(bool busy) -{ - busy_ = busy; - saveAction_->setEnabled(!busy && !processedImage_.empty()); - saveAsAction_->setEnabled(!busy && !processedImage_.empty()); - if (busy) { - statusLabel_->setText(QStringLiteral("处理中...")); - } -} - -bool MainWindow::ensureHasImage() const -{ - if (processedImage_.empty()) { - QMessageBox::information(const_cast(this), QStringLiteral("提示"), QStringLiteral("请先打开图片。")); - return false; - } - return true; -} - -void MainWindow::requestProcess(ImageWorker::Operation operation, double value, int dx, int dy) -{ - if (busy_) { - return; - } - if (!ensureHasImage()) { - return; - } - setBusy(true); - Q_EMIT processRequested(processedImage_.clone(), operation, value, dx, dy); -} - -bool MainWindow::writeCurrentImageTo(const QString &path) -{ - if (processedImage_.empty()) { - return false; - } - - const std::string localPath = QFile::encodeName(path).toStdString(); - if (!cv::imwrite(localPath, processedImage_)) { - QMessageBox::warning(this, QStringLiteral("保存失败"), QStringLiteral("无法保存图片:%1").arg(path)); - return false; - } - - statusLabel_->setText(QStringLiteral("已保存图片")); - infoLabel_->setText(QStringLiteral("已保存处理后图片:%1").arg(path)); - return true; -} diff --git a/app/src/main.cpp b/app/src/main.cpp deleted file mode 100644 index ad3d3d2..0000000 --- a/app/src/main.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include - -#include "MainWindow.h" - -int main(int argc, char *argv[]) -{ - QApplication app(argc, argv); - QApplication::setApplicationName("chengnan"); - QApplication::setOrganizationName("luochen570"); - QApplication::setApplicationVersion(CHENGNAN_APP_VERSION); - - qRegisterMetaType("cv::Mat"); - - MainWindow window; - window.resize(1280, 760); - window.show(); - - return QApplication::exec(); -} diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..34dae4d --- /dev/null +++ b/main.cpp @@ -0,0 +1,16 @@ +#include "MainWindow.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QApplication::setApplicationName(QStringLiteral("chengnan")); + QApplication::setApplicationVersion(QStringLiteral(CHENGNAN_APP_VERSION)); + QApplication::setOrganizationName(QStringLiteral("luochen570")); + + MainWindow window; + window.show(); + + return QApplication::exec(); +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..6cdb05c --- /dev/null +++ b/readme.md @@ -0,0 +1,55 @@ +# chengnan Dev 分支 + +这是 `chengnan` 的简化开发分支,只保留根目录下的最小 Qt Widgets 源码文件: + +- `CMakeLists.txt` +- `main.cpp` +- `MainWindow.h` +- `MainWindow.cpp` +- `readme.md` + +## 功能 + +当前实现了一个轻量级图片查看器 / 简单图片处理工具: + +- 打开图片:PNG、JPG、JPEG、BMP、GIF、WebP 等 Qt 支持的格式 +- 保存当前图片 +- 另存为 PNG / JPG / BMP 等格式 +- 图片显示:使用 Qt Widgets,不使用 OpenCV 窗口 +- 缩放:工具栏滑块、放大、缩小、原始大小 +- 双击图片适配窗口 +- 旋转:左旋 90°、右旋 90° +- 翻转:水平翻转、垂直翻转 +- 简单处理:灰度化、反色 + +## 技术栈 + +- C++17 +- Qt 5 或 Qt 6 Widgets +- CMake + +此分支不依赖 OpenCV,便于保持文件数量最少。 + +## 构建方式 + +本地已安装 Qt 和 CMake 后,可执行: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release +``` + +如果 Qt 安装在非默认路径,可指定: + +```bash +cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/lib/cmake +cmake --build build --config Release +``` + +## 文件说明 + +- `main.cpp`:应用入口,创建并显示主窗口 +- `MainWindow.h`:主窗口和图片标签类声明 +- `MainWindow.cpp`:菜单、工具栏、图片显示和处理逻辑 +- `CMakeLists.txt`:Qt Widgets CMake 构建配置 +- `readme.md`:当前说明文档