226 lines
6.9 KiB
Markdown
226 lines
6.9 KiB
Markdown
# 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`。
|
||
|
||
**问题 2:Ctrl-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 默认 Wayland,PySide6 如果找不到 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`(headless)API 完全兼容,避免调用方因类型不同而崩溃。
|
||
|
||
---
|
||
|
||
### 第 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()` 方法 |
|