终于找到了把Ursina窗口嵌入到Tkinter窗口的方法

159 阅读3分钟

终于找到了把Ursina窗口嵌入到Tkinter窗口的方法

在开发3D可视化应用时,我们常常希望将3D渲染窗口集成到现有的GUI框架中。本文将介绍如何将Ursina引擎的3D窗口完美嵌入到Tkinter应用程序中,实现两种GUI框架的无缝整合。


环境准备

在运行代码前,请确保:

  1. 安装Python 3.12+(推荐最新版本)
  2. 安装Ursina引擎:pip install ursina
  3. 安装PyWin32:pip install pywin32
  4. 关键步骤:安装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()

关键技术解析

  1. 窗口嵌入原理

    • 使用win32gui.SetParent()Ursina窗口设置为Tkinter Frame的子窗口
    • 通过win32gui.SetWindowLong()移除窗口边框
    • 使用win32gui.MoveWindow()动态调整窗口大小
  2. 双框架协同工作

    • Tkinter处理2D GUI元素(按钮、布局等)
    • Ursina专注3D渲染(星空、相机控制)
    • 通过root.after()在主线程中同步更新
  3. 输入焦点管理

    • 点击Frame区域时将焦点切换到Ursina窗口
    • 确保键盘输入(WASD)能正确传递到3D场景
  4. 相机控制系统

    • WASD控制前后左右移动
    • Q/E控制垂直移动
    • 鼠标控制视角旋转

运行效果

程序启动后将显示:

  1. 主窗口包含Tkinter按钮和黑色Frame区域
  2. Frame内嵌Ursina渲染的3D星空场景
  3. 黄色太阳位于场景右侧
  4. 可通过鼠标和键盘自由控制相机移动

注意事项

  1. 必须执行:安装PyWin32后运行python pywin32_postinstall.py -install
  2. 窗口嵌入仅在Windows系统有效
  3. 使用Ursina最新版本避免兼容性问题

通过这种嵌入方式,开发者可以创建复杂的混合界面,将Ursina的强大3D渲染能力与Tkinter丰富的2D控件结合,为科学可视化、游戏开发等场景提供新的可能性。