Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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同一目录下,需要添加搜索路径

# 设置版本信息变量
Expand Down Expand Up @@ -83,16 +83,16 @@ target_link_libraries(Win_Switcher

target_link_libraries(${PROJECT_NAME}
Dwmapi
WindowsApp
)

# 启用`gui_private`模块
# 必须放在find_package之后,否则`Qt6Gui_PRIVATE_INCLUDE_DIRS`变量未定义
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
Expand Down
108 changes: 108 additions & 0 deletions CONFIG.md
Original file line number Diff line number Diff line change
@@ -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) |
107 changes: 107 additions & 0 deletions CONFIG_zh.md
Original file line number Diff line number Diff line change
@@ -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 方向键(← ↓ ↑ →) |
| `←` `→` | 左右移动选中项 |
| `↑` `↓` / 鼠标滚轮 | 在同一应用组内切换窗口 |
21 changes: 21 additions & 0 deletions header/utils/ConfigManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ 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() {
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) {}
};
Expand Down
1 change: 1 addition & 0 deletions header/utils/ConfigManagerBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <QSettings>
#include <QProcess>
#include <QFile>

class ConfigManagerBase : public QObject {
Q_OBJECT
Expand Down
14 changes: 9 additions & 5 deletions src/utils/Util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "utils/Util.h"
#include <appmodel.h>
#include "utils/AppUtil.h"
#include "utils/ConfigManager.h"
#include <QDebug>
#include <psapi.h>
#include <QFileInfo>
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
57 changes: 41 additions & 16 deletions src/widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,33 @@ QList<WindowGroup> 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<WindowGroup> 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;
}

Expand Down Expand Up @@ -330,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);
}
Expand Down