Files
EzVibe/docs/linux-qt-fix.md

226 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Linux Qt 桌面宠物体修复记录
## 问题描述
Fedora 43 上运行 `python3 main.py` 时:
1. **Ctrl-C 无法停止** — 程序忽略 SIGINT窗口也看不见
2. **无任何窗口出现**`python3 main.py` 没有任何输出就崩溃或无响应
3. **错误信息模糊** — 无明确报错,难以定位根因
## 修复过程4 轮迭代)
---
### 第 1 轮:包名错误 + Ctrl-C 无法停止
**问题 1包名错误**
```bash
sudo dnf install qt5-qtbase-xcb
# Error: No match for argument: qt5-qtbase-xcb
```
Fedora 43 默认使用 **Qt6**,而代码安装的是 PySide6 (Qt6)。包名应为 `qt6-qtbase-xcb`,不是 `qt5`
**问题 2Ctrl-C 无法停止**
`app.exec()` 阻塞主线程Python 默认不处理 `SIGINT`。需要显式注册信号处理器。
**修复** (`main.py`)
```python
import signal
def _run_qt(self) -> None:
# ...
def _sigint_handler(signum, frame):
print("\n[EzVibe] 收到 Ctrl+C正在关闭 ...")
app.quit()
signal.signal(signal.SIGINT, _sigint_handler)
app.exec()
```
同时在 `main.py` 开头(在任何 Qt 导入之前)设置平台环境变量:
```python
if sys.platform.startswith("linux") and os.environ.get("QT_QPA_PLATFORM") is None:
os.environ["QT_QPA_PLATFORM"] = "xcb"
```
**原因**Fedora 43 默认 WaylandPySide6 如果找不到 XCB 平台插件会静默失败(窗口根本不创建)。`QT_QPA_PLATFORM` 必须在任何 Qt 模块导入之前设置。
---
### 第 2 轮:`DummyPetWindow` API 不兼容
**问题**`python3 main.py --dummy` 正常,但全量模式报错:
```
AttributeError: 'DummyPetWindow' object has no attribute 'isVisible'
```
`DummyPetWindow` 用的是 snake_case 方法名(`is_visible()`),但 `main.py` 里调用的是 Qt 风格的 camelCase`isVisible()`)。
**修复** (`ui/pet_window.py`)
```python
def is_visible(self) -> bool:
return self._visible
# Qt API 别名
isVisible = is_visible
```
同时补充 `geometry()` 方法(同样被 `main.py` 调试输出调用):
```python
def geometry(self):
"""返回模拟的 QRect。"""
class _DummyRect:
def __init__(self, x, y, w, h):
self._x, self._y, self._w, self._h = x, y, w, h
def x(self): return self._x
def y(self): return self._y
def width(self): return self._w
def height(self): return self._h
def right(self): return self._x + self._w
def top(self): return self._y
def __repr__(self):
return f"QRect({self._x}, {self._y}, {self._w}, {self._h})"
return _DummyRect(self._position[0], self._position[1], 200, 200)
```
**原则**:保持 `PetWindow`(真 Qt`DummyPetWindow`headlessAPI 完全兼容,避免调用方因类型不同而崩溃。
---
### 第 3 轮:`_load_qt()` 检测逻辑错误
**问题**`create_pet_window()` 总是返回 `DummyPetWindow`,从未成功创建真正的 Qt 窗口。
**根因**`__import__()` 的行为:
```python
# 旧代码(错误)
mod = __import__("PySide6") # 返回顶层级:<module 'PySide6'>
getattr(mod, "QtCore") # PySide6 没有 QtCore 属性 → 抛出 AttributeError
# → _load_qt() 返回 None → 回退到 DummyPetWindow
```
`__import__("PySide6")` 只返回顶级模块,不会加载子模块 `QtCore`/`QtWidgets`。所以 `getattr(mod, "QtCore")` 永远失败。
**修复** (`ui/pet_window.py`)
```python
def _load_qt() -> str | None:
global _Qt
if _Qt is not None:
return _Qt
for lib in ("PySide6", "PyQt6"):
try:
# 直接 import 子模块fromlist=[""] 触发完整路径导入
__import__(f"{lib}.QtCore", fromlist=[""])
__import__(f"{lib}.QtWidgets", fromlist=[""])
_Qt = lib
return lib
except (ImportError, AttributeError):
continue
return None
```
**关键**`fromlist=[""]` 使得 `__import__()` 返回最右边的模块(即 `QtCore`/`QtWidgets`),真正触发子模块的加载和验证。
---
### 第 4 轮:`QApplication` 未创建时创建了 `QWidget`
**问题**
```
QWidget: Must construct a QApplication before a QWidget
Aborted (core dumped)
```
**根因**:之前的架构中,`create_pet_window()``EzVibeApp.__init__()` 里被调用,而 `QApplication``_run_qt()` 里才创建。Qt 要求所有 widget 必须在 `QApplication` 之后才能实例化。
**修复** (`main.py`)
```python
def __init__(...):
# ...
# 6. 桌宠窗口(延迟创建,见 _run_qt
self._window = None # 不再在这里调用 create_pet_window
def _run_qt(self) -> None:
app = QtWidgets.QApplication(sys.argv)
# ...
# ★ 现在 QApplication 已存在,创建桌宠窗口
from ui.pet_window import create_pet_window
self._window = create_pet_window(
emotion_engine=self._emotion,
brain=self._brain,
scheduler=self._scheduler,
assets_dir=self._assets_dir,
force_dummy=self._use_dummy,
)
self._window.show()
app.exec()
```
**关键**Qt 的 `QApplication` 是全局单例,必须是第一个被创建的 Qt 对象,之后才能实例化任何 widget。这是 Qt 的硬性要求,与平台无关。
---
## 最终结果
```
[EzVibe] 初始化组件 ...
[✓] EmotionEngine: idle
[✓] VectorMemory initialized
[✓] AgentBrain: dummy
[✓] KeyboardMouseMonitor started
[✓] BehaviorScheduler initialized
[EzVibe] 启动中 ... (mode=full)
[EzVibe] Qt 平台: xcb
[✓] PetWindow: PetWindow (Qt=PySide6)
[EzVibe] 桌宠已显示。右键点击切换情绪。
[EzVibe] 按 Ctrl+C 或关闭窗口退出。
```
窗口成功显示在屏幕上。
## Linux 部署检查清单
如果在新环境部署,按以下顺序检查:
1. **安装 PySide6**`pip install PySide6`
2. **安装 Qt6 XCB 平台插件**
- Fedora: `sudo dnf install qt6-qtbase-xcb`
- Ubuntu/Debian: `sudo apt install libxkbcommon-x11-0`XCB 依赖)
3. **验证 Qt 平台**`python3 -c "from PySide6 import QtGui; print(QtGui.QGuiApplication.platformName())"`
- 期望输出:`xcb`
- 如果输出 `offscreen` 或报错,说明 XCB 插件未装好
4. **测试窗口**
```bash
python3 -c "
import os, sys
os.environ['QT_QPA_PLATFORM'] = 'xcb'
from PySide6 import QtWidgets
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
w.setStyleSheet('background: rgba(200,100,100,200);')
w.resize(200,200)
w.show()
app.exec()
```
- 如果有任何错误,检查第 2 步
5. **运行桌宠**`python3 main.py`
6. **退出**`Ctrl+C` 或关闭窗口
## 修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `main.py` | 早期设置 `QT_QPA_PLATFORM=xcb`;延迟 PetWindow 创建到 `QApplication` 之后SIGINT 处理器;调试输出 |
| `ui/pet_window.py` | 修复 `_load_qt()` 使用正确的 `__import__` 方式;`DummyPetWindow` 添加 `isVisible` 别名和 `geometry()` 方法 |