终于找到了把Ursina窗口嵌入到Tkinter窗口的方法
在开发3D可视化应用时,我们常常希望将3D渲染窗口集成到现有的GUI框架中。本文将介绍如何将Ursina引擎的3D窗口完美嵌入到Tkinter应用程序中,实现两种GUI框架的无缝整合。
环境准备
在运行代码前,请确保:
- 安装Python 3.12+(推荐最新版本)
- 安装Ursina引擎:
pip install ursina - 安装PyWin32:
pip install pywin32 - 关键步骤:安装PyWin32后执行:
python pywin32_postinstall.py -install
代码解析
以下代码实现了Ursina窗口嵌入Tkinter框架的核心功能:
import tkinter as tk
from tkinter import Frame
from ursina import * # Ursina 3D引擎
from random import random
import traceback
import math
import win32gui # Windows API操作
import win32con # Windows常量
import sys
# 创建Tkinter主窗口
root = tk.Tk()
root.title("Ursina嵌入Tkinter")
root.geometry("800x600")
def on_closing():
"""处理窗口关闭事件"""
application.quit() # 关闭Ursina
root.destroy() # 关闭Tkinter
sys.exit() # 退出程序
root.protocol("WM_DELETE_WINDOW", on_closing)
# 创建用于嵌入Ursina的黑色Frame
frame = Frame(root, bg='black', width=600, height=400)
frame.pack(pady=20, padx=20, fill=tk.BOTH, expand=True)
# 示例Tkinter按钮
button = tk.Button(root, text="Tkinter按钮", command=lambda: print("Tk按钮被点击"))
button.pack(pady=10)
def init_ursina():
global app, hwnd
try:
# 初始化Ursina(无边框窗口)
app = Ursina(borderless=True, fullscreen=False, title="Embedded Ursina")
# 创建星空背景(500个随机位置的球体)
distance = 1000
for i in range(500):
dec = 360 * random()
ra = 360 * random()
Entity(
model='sphere',
color='#ffffff',
position=(
distance * math.cos(math.radians(dec)) * math.cos(math.radians(ra)),
distance * math.sin(math.radians(dec)),
distance * math.cos(math.radians(dec)) * math.sin(math.radians(ra))
)
)
# 创建黄色太阳
Entity(model='sphere', scale=50, position=(100, 0, 0), color=color.yellow)
# 设置相机初始位置
camera.position = (0, 0, -300)
# 相机移动控制
speed = 50
sensitivity = 20
def camera_update():
direction = Vec3(
camera.forward * (held_keys['w'] - held_keys['s']) +
camera.right * (held_keys['d'] - held_keys['a']) +
camera.up * (held_keys['e'] - held_keys['q'])
).normalized()
camera.position += direction * speed * time.dt
camera.rotation_y += mouse.velocity[0] * sensitivity
camera.rotation_x -= mouse.velocity[1] * sensitivity
camera.update = camera_update
except Exception as e:
print(f"初始化Ursina时出错: {e}")
traceback.print_exc()
return
# 关键步骤:获取Ursina窗口句柄
window_handle = base.win.getWindowHandle()
hwnd = window_handle.getIntHandle()
# 将Ursina窗口设置为Tkinter Frame的子窗口
win32gui.SetParent(hwnd, frame.winfo_id())
# 修改窗口样式(移除边框)
style = win32con.WS_CHILD | win32con.WS_VISIBLE
win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, style)
def adjust_window_size():
"""动态调整Ursina窗口大小匹配Frame"""
frame.update_idletasks()
width = frame.winfo_width()
height = frame.winfo_height()
if width > 0 and height > 0:
win32gui.MoveWindow(hwnd, 0, 0, width, height, True)
# 初始调整
adjust_window_size()
# 绑定窗口大小变化事件
def on_resize(event):
adjust_window_size()
frame.bind("<Configure>", on_resize)
# 点击Frame时聚焦到Ursina窗口
def set_ursina_focus(event):
win32gui.SetFocus(hwnd)
frame.bind("<Button-1>", set_ursina_focus)
# Tkinter主线程中更新Ursina
def update_ursina():
base.step() # 单步更新Ursina
root.after(10, update_ursina) # 每10ms更新一次
root.after(10, update_ursina)
# 最大化窗口
root.after(10, lambda: root.state('zoomed'))
# 延迟初始化Ursina(确保Tkinter完全加载)
frame.after(100, init_ursina)
# 启动Tkinter主循环
root.mainloop()
关键技术解析
-
窗口嵌入原理:
- 使用
win32gui.SetParent()将Ursina窗口设置为Tkinter Frame的子窗口 - 通过
win32gui.SetWindowLong()移除窗口边框 - 使用
win32gui.MoveWindow()动态调整窗口大小
- 使用
-
双框架协同工作:
Tkinter处理2D GUI元素(按钮、布局等)Ursina专注3D渲染(星空、相机控制)- 通过
root.after()在主线程中同步更新
-
输入焦点管理:
- 点击
Frame区域时将焦点切换到Ursina窗口 - 确保键盘输入(WASD)能正确传递到3D场景
- 点击
-
相机控制系统:
- WASD控制前后左右移动
- Q/E控制垂直移动
- 鼠标控制视角旋转
运行效果
程序启动后将显示:
- 主窗口包含
Tkinter按钮和黑色Frame区域 Frame内嵌Ursina渲染的3D星空场景- 黄色太阳位于场景右侧
- 可通过鼠标和键盘自由控制相机移动
注意事项
- 必须执行:安装
PyWin32后运行python pywin32_postinstall.py -install - 窗口嵌入仅在Windows系统有效
- 使用
Ursina最新版本避免兼容性问题
通过这种嵌入方式,开发者可以创建复杂的混合界面,将Ursina的强大3D渲染能力与Tkinter丰富的2D控件结合,为科学可视化、游戏开发等场景提供新的可能性。