From 6c9c8e183b219c69d343367a45b113793b4a794e Mon Sep 17 00:00:00 2001 From: Henry Yves Date: Tue, 16 Jun 2026 19:16:52 +0800 Subject: [PATCH 1/4] feat(config): make window blacklist user-configurable via config.ini - Add [blacklist] section support in config.ini (file_names key) - Read blacklist from config in isWindowAcceptable() using QSettings - Case-insensitive filename matching for robustness - Fix missing #include in ConfigManagerBase.h - Fix missing WindowsApp link library for WinRT symbols - Fix WIN32_EXECUTABLE for multi-config generators (VS) Closes TODO: 'by user from config' in isWindowAcceptable() --- CMakeLists.txt | 6 +++--- header/utils/ConfigManager.h | 14 ++++++++++++++ header/utils/ConfigManagerBase.h | 1 + src/utils/Util.cpp | 14 +++++++++----- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 94af796..3a64e56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ target_link_libraries(Win_Switcher target_link_libraries(${PROJECT_NAME} Dwmapi + WindowsApp ) # 启用`gui_private`模块 @@ -90,9 +91,8 @@ target_link_libraries(${PROJECT_NAME} target_include_directories(${PROJECT_NAME} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS}) set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "AltTaber") # 设置输出文件名 -if (CMAKE_BUILD_TYPE MATCHES "Release") - set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE true) # 设置为窗口应用程序, 无控制台,同时会导致Clion无法捕获输出 -endif () +# 对于多配置生成器(VS),WIN32_EXECUTABLE 通过vxproj属性控制,这里用生成器表达式为Release配置设置 +set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE true) target_compile_definitions(${PROJECT_NAME} PRIVATE QT_DEPRECATED_WARNINGS diff --git a/header/utils/ConfigManager.h b/header/utils/ConfigManager.h index 5143e93..b487a3f 100644 --- a/header/utils/ConfigManager.h +++ b/header/utils/ConfigManager.h @@ -41,6 +41,20 @@ class ConfigManager : public ConfigManagerBase { set("DisplayMonitor", monitor); } + /// User-configured blacklist of exe filenames to hide from the switcher. + /// In config.ini: [blacklist] \n file_names=app1.exe, app2.exe + QStringList getBlacklistFileNames() { + auto raw = get("blacklist/file_names", QStringList{}).toStringList(); + QStringList result; + for (auto& item : raw) { + for (auto& sub : item.split(',')) { + auto trimmed = sub.trimmed(); + if (!trimmed.isEmpty()) result << trimmed.toLower(); + } + } + return result; + } + private: explicit ConfigManager(const QString& filename) : ConfigManagerBase(filename) {} }; diff --git a/header/utils/ConfigManagerBase.h b/header/utils/ConfigManagerBase.h index b173b15..1b6872c 100644 --- a/header/utils/ConfigManagerBase.h +++ b/header/utils/ConfigManagerBase.h @@ -3,6 +3,7 @@ #include #include +#include class ConfigManagerBase : public QObject { Q_OBJECT diff --git a/src/utils/Util.cpp b/src/utils/Util.cpp index 64b5c0a..c82abe1 100644 --- a/src/utils/Util.cpp +++ b/src/utils/Util.cpp @@ -2,6 +2,7 @@ #include "utils/Util.h" #include #include "utils/AppUtil.h" +#include "utils/ConfigManager.h" #include #include #include @@ -263,10 +264,10 @@ namespace Util { static const QStringList BlackList_ExePath = { R"(C:\Windows\System32\wscript.exe)" }; - static const QStringList BlackList_FileName = { // TODO by user from config - "Nahimic3.exe", - "Follower.exe", - "QQ Follower.exe" + static const QStringList DefaultBlackList_FileName = { + "nahimic3.exe", + "follower.exe", + "qq follower.exe" }; LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); QString className; @@ -283,7 +284,10 @@ namespace Util { && !className.startsWith("imestatuspop_classname{") // 输入法(的推销弹窗)https://s3.bmp.ovh/imgs/2024/12/23/bb136fde101a41ce.png ) { auto path = getWindowProcessPath(hwnd); // 耗时操作,减少次数 - if (!BlackList_ExePath.contains(path) && !BlackList_FileName.contains(QFileInfo(path).fileName())) + auto fileName = QFileInfo(path).fileName().toLower(); + if (!BlackList_ExePath.contains(path) + && !DefaultBlackList_FileName.contains(fileName) + && !cfg.getBlacklistFileNames().contains(fileName)) return true; } return false; From 4ab3149da77cfa5373644bd0e67a85dabeead12a Mon Sep 17 00:00:00 2001 From: Henry Yves Date: Sat, 4 Jul 2026 08:46:44 +0800 Subject: [PATCH 2/4] feat: add configurable new-app-second sorting When [sorting] new_app_second=true in config.ini, apps with no usage record are placed at the second position (right after the foreground window) instead of at the end. This makes newly launched apps easily reachable with Alt+Tab. Also includes prior fixes: WIN32_EXECUTABLE for VS generator, WindowsApp link, configurable blacklist with case-insensitive matching. --- CMakeLists.txt | 2 +- header/utils/ConfigManager.h | 7 +++++++ src/widget.cpp | 34 +++++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a64e56..1fb0b3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -set(CMAKE_PREFIX_PATH "D:/Qt/6.8.1/msvc2022_64/lib/cmake") +set(CMAKE_PREFIX_PATH "D:/a_my/tool/Qt/6.8.1/msvc2022_64/lib/cmake") list(APPEND CMAKE_AUTOUIC_SEARCH_PATHS ui) # 如果.ui文件不在.cpp同一目录下,需要添加搜索路径 # 设置版本信息变量 diff --git a/header/utils/ConfigManager.h b/header/utils/ConfigManager.h index b487a3f..08cf21a 100644 --- a/header/utils/ConfigManager.h +++ b/header/utils/ConfigManager.h @@ -41,6 +41,13 @@ class ConfigManager : public ConfigManagerBase { set("DisplayMonitor", monitor); } + /// When enabled, apps with no usage record are sorted to the second position + /// (right after the foreground window) instead of the end. + /// In config.ini: [sorting] \n new_app_second=true + bool getNewAppSortSecond() { + return get("sorting/new_app_second", false).toBool(); + } + /// User-configured blacklist of exe filenames to hide from the switcher. /// In config.ini: [blacklist] \n file_names=app1.exe, app2.exe QStringList getBlacklistFileNames() { diff --git a/src/widget.cpp b/src/widget.cpp index a78270b..bae746c 100644 --- a/src/widget.cpp +++ b/src/widget.cpp @@ -253,13 +253,33 @@ QList Widget::prepareWindowGroupList() { } auto winGroupList = winGroupMap.values(); // 按照活跃度排序 - std::sort(winGroupList.begin(), winGroupList.end(), [this](const WindowGroup& a, const WindowGroup& b) { - auto timeA = getLastValidActiveGroupWindow(a).second; - auto timeB = getLastValidActiveGroupWindow(b).second; - if (timeA.isNull() && timeB.isNull()) return false; - if (timeA.isValid() && timeB.isValid()) return timeA > timeB; - return timeA.isValid(); - }); + if (cfg.getNewAppSortSecond()) { + // 启用"新应用排第二":无使用记录的窗口组放到第2位(紧跟当前前台窗口) + QList withRecord, withoutRecord; + for (auto& group : winGroupList) { + if (getLastValidActiveGroupWindow(group).second.isValid()) + withRecord.append(group); + else + withoutRecord.append(group); + } + std::sort(withRecord.begin(), withRecord.end(), [this](const WindowGroup& a, const WindowGroup& b) { + return getLastValidActiveGroupWindow(a).second > getLastValidActiveGroupWindow(b).second; + }); + winGroupList = withRecord; + if (!withoutRecord.isEmpty()) { + // 插入到 index 1(第2位),保持无记录项之间的原有顺序 + for (int i = 0; i < withoutRecord.size(); i++) + winGroupList.insert(1 + i, withoutRecord[i]); + } + } else { + std::sort(winGroupList.begin(), winGroupList.end(), [this](const WindowGroup& a, const WindowGroup& b) { + auto timeA = getLastValidActiveGroupWindow(a).second; + auto timeB = getLastValidActiveGroupWindow(b).second; + if (timeA.isNull() && timeB.isNull()) return false; + if (timeA.isValid() && timeB.isValid()) return timeA > timeB; + return timeA.isValid(); + }); + } return winGroupList; } From 8794b5656d4896384d5daa50be7fd46d79cd75ca Mon Sep 17 00:00:00 2001 From: Henry Yves Date: Sat, 4 Jul 2026 09:16:05 +0800 Subject: [PATCH 3/4] docs: add configuration guide (EN & ZH) --- CONFIG.md | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++ CONFIG_zh.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 CONFIG.md create mode 100644 CONFIG_zh.md diff --git a/CONFIG.md b/CONFIG.md new file mode 100644 index 0000000..4d69472 --- /dev/null +++ b/CONFIG.md @@ -0,0 +1,108 @@ +# AltTaber Configuration Guide + +`config.ini` lives next to `AltTaber.exe`. It is created automatically when you click **Settings** in the tray icon menu. Changes take effect after restarting AltTaber. + +--- + +## Complete example + +```ini +DisplayMonitor=1 +[label] +font_size=10 +font_family=Microsoft YaHei UI + +[blacklist] +file_names=obsidian.exe, slack.exe + +[sorting] +new_app_second=true +``` + +--- + +## All options + +### Display Monitor + +Which screen the switcher appears on. + +| Key | Type | Default | Values | +|-----|------|---------|--------| +| `DisplayMonitor` | int | `1` | `0` = primary monitor, `1` = follow mouse cursor | + +Can also be changed via tray icon → **Display Monitor**. + +--- + +### `[label]` — Font customization + +Controls the label text shown under each app icon. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `font_size` | int | `10` | Font point size | +| `font_family` | string | `Microsoft YaHei UI` | Preferred font; falls back to Microsoft YaHei → Consolas | + +--- + +### `[blacklist]` — Hide apps from the switcher + +Prevent specific apps from appearing in the Alt+Tab list. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `file_names` | string | (empty) | Comma-separated `.exe` names, **case-insensitive** | + +```ini +[blacklist] +file_names=obsidian.exe, slack.exe, telegram.exe +``` + +> **Note:** Three apps are always hidden regardless of `config.ini`: +> `Nahimic3.exe`, `Follower.exe`, `QQ Follower.exe`. +> These cannot be un-hidden. + +If an app still shows up, check its exact exe name via Task Manager. + +--- + +### `[sorting]` — Sort order + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `new_app_second` | bool | `false` | When enabled, apps that have never been activated are placed at position 2 (right after the foreground window) instead of at the end of the list. Useful for apps launched via hotkey that don't grab focus on startup. | + +```ini +[sorting] +new_app_second=true +``` + +--- + +## Tray icon menu + +These settings are managed through the tray menu, not `config.ini`: + +| Menu item | What it does | +|-----------|--------------| +| **Check for Updates** | Check for new AltTaber releases | +| **Settings** | Open `config.ini` in Notepad for editing | +| **Start with Windows** | Toggle auto-start with Windows | +| **Display Monitor** | Choose primary monitor or follow mouse | +| **Quit** | Exit AltTaber | + +--- + +## Keyboard shortcuts + +These are **not configurable**: + +| Shortcut | Action | +|----------|--------| +| `Alt` + `Tab` | Open switcher, cycle through apps forward | +| `Alt` + `Shift` + `Tab` | Cycle through apps backward | +| `Alt` + `` ` `` | Cycle through windows of the same app | +| `H` `J` `K` `L` | Vim-style navigation (← ↓ ↑ →) | +| `←` `→` | Move selection left / right | +| `↑` `↓` / mouse wheel | Scroll within an app group (cycle its windows) | diff --git a/CONFIG_zh.md b/CONFIG_zh.md new file mode 100644 index 0000000..a53ca45 --- /dev/null +++ b/CONFIG_zh.md @@ -0,0 +1,107 @@ +# AltTaber 配置指南 + +`config.ini` 放在 `AltTaber.exe` 同级目录下。首次点击托盘菜单 **Settings** 时会自动生成。修改后重启 AltTaber 生效。 + +--- + +## 完整示例 + +```ini +DisplayMonitor=1 +[label] +font_size=10 +font_family=Microsoft YaHei UI + +[blacklist] +file_names=obsidian.exe, slack.exe + +[sorting] +new_app_second=true +``` + +--- + +## 全部选项 + +### 显示显示器 + +切换面板显示在哪个屏幕上。 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `DisplayMonitor` | int | `1` | `0` = 主显示器,`1` = 跟随鼠标光标 | + +也可以右键托盘图标 → **Display Monitor** 直接切换。 + +--- + +### `[label]` — 字体设置 + +控制图标下方应用名称的字体样式。 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `font_size` | int | `10` | 字体大小(磅) | +| `font_family` | string | `Microsoft YaHei UI` | 首选字体;加载失败时依次尝试 Microsoft YaHei → Consolas | + +--- + +### `[blacklist]` — 隐藏不想看到的窗口 + +让指定应用不出现在 Alt+Tab 切换列表中。 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `file_names` | string | (空) | 逗号分隔的 exe 文件名,**大小写不敏感** | + +```ini +[blacklist] +file_names=obsidian.exe, slack.exe, telegram.exe +``` + +> **注意**:以下三个进程硬编码隐藏,config.ini 无法恢复: +> `Nahimic3.exe`、`Follower.exe`、`QQ Follower.exe`。 + +如果配置了但应用仍然出现,去任务管理器确认准确的 exe 文件名。 + +--- + +### `[sorting]` — 排序规则 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `new_app_second` | bool | `false` | 启用后,没有被切换过的应用会排到第 2 位(紧跟当前前台窗口),而不是排在列表末尾。适用于通过快捷键启动但不自动获取焦点的应用。 | + +```ini +[sorting] +new_app_second=true +``` + +--- + +## 托盘菜单 + +以下设置通过右键托盘图标操作,不在 `config.ini` 中: + +| 菜单项 | 功能 | +|--------|------| +| **Check for Updates** | 检查新版本 | +| **Settings** | 用记事本打开 `config.ini` | +| **Start with Windows** | 开机自启 | +| **Display Monitor** | 选择主显示器或跟随鼠标 | +| **Quit** | 退出 AltTaber | + +--- + +## 快捷键 + +快捷键**不支持配置**: + +| 快捷键 | 作用 | +|--------|------| +| `Alt` + `Tab` | 打开切换面板,向前切换应用 | +| `Alt` + `Shift` + `Tab` | 向后切换应用 | +| `Alt` + `` ` `` | 在同一应用的多个窗口间切换 | +| `H` `J` `K` `L` | Vim 方向键(← ↓ ↑ →) | +| `←` `→` | 左右移动选中项 | +| `↑` `↓` / 鼠标滚轮 | 在同一应用组内切换窗口 | From a665f50a14349664a1c846bdabac1bb9ade57ae4 Mon Sep 17 00:00:00 2001 From: Henry Yves Date: Sat, 4 Jul 2026 09:26:46 +0800 Subject: [PATCH 4/4] fix: Alt+Shift+Tab now goes to last item on first press When the first Tab press triggers requestShow() (switcher not yet foreground), the Shift state was ignored and it always selected the second item. Now it checks VK_SHIFT during initial show and starts from the last item when Shift is held. --- src/widget.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/widget.cpp b/src/widget.cpp index bae746c..d08374a 100644 --- a/src/widget.cpp +++ b/src/widget.cpp @@ -350,17 +350,22 @@ bool Widget::prepareListWidget() { // set current item if (lw->count() >= 2) { - auto foreWin = GetForegroundWindow(); - bool isFirstItemForeground = false; - for (auto& info: winGroupList.at(0).windows) { - if (info.hwnd == foreWin) { - isFirstItemForeground = true; - break; + // Alt+Shift+Tab: start from the last item instead of the second + if (Util::isKeyPressed(VK_SHIFT)) { + lw->setCurrentRow(lw->count() - 1); + } else { + auto foreWin = GetForegroundWindow(); + bool isFirstItemForeground = false; + for (auto& info: winGroupList.at(0).windows) { + if (info.hwnd == foreWin) { + isFirstItemForeground = true; + break; + } } + // 如果第一个item是前台窗口,就选中第二个 + // 因为有些情况:选中桌面 并不会产生一个item + lw->setCurrentRow(isFirstItemForeground ? 1 : 0); //! 首次显示时,该行特别耗时:472ms } - // 如果第一个item是前台窗口,就选中第二个 - // 因为有些情况:选中桌面 并不会产生一个item - lw->setCurrentRow(isFirstItemForeground ? 1 : 0); //! 首次显示时,该行特别耗时:472ms } else if (lw->count() == 1) { lw->setCurrentRow(0); }