You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kivy开发ChatGPT客户端:聊天气泡位置错位问题求助

Kivy ChatGPT客户端聊天气泡错位、相邻排列问题修复方案

问题根源分析

  • 主界面ChatInterface错误设置为横向布局,导致输入栏与聊天记录区域并排显示,引发整体布局错位
  • 聊天气泡未根据消息角色区分对齐方式,所有气泡挤在同一侧
  • 文本标签的宽度限制逻辑错误,导致内容溢出或气泡尺寸异常
  • 聊天记录容器缺少内边距和间距,气泡贴边且无间隔

修复后的完整代码

import requests
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import ObjectProperty, StringProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label

# 窗口配置
from kivy.config import Config
Config.set('graphics', 'width', '400')
Config.set('graphics', 'height', '600')
Config.set('graphics', 'borderless', 0)
Config.set('graphics', 'resizable', 0)
Config.set('graphics', 'window_state', 'visible')
Config.set('graphics', 'minimum_width', '400')
Config.set('graphics', 'minimum_height', '600')

# 替换为你的OpenAI API地址和密钥
OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"
OPENAI_API_KEY = "YOUR_API_KEY"

Builder.load_string("""
<ChatBubble>:
    size_hint_y: None
    height: self.minimum_height
    padding: 10

    FloatLayout:
        size_hint_x: None
        width: min(root.texture_width + 40, root.parent.width - 40) if root.parent else min(root.texture_width + 40, 360)
        pos_hint: {"x": 0} if root.role == "bot" else {"right": 1}
        height: root.height

        canvas.before:
            Color:
                rgba: (40/255,116/255,166/255, 1) if root.role == "bot" else (0.15, 0.15, 0.15, 1)
            RoundedRectangle:
                pos: self.pos
                size: self.size
                radius: [20, 20, 20, 20] if root.role == "bot" else [20, 20, 0, 20]

        Label:
            id: message_label
            text: root.text
            color: (1, 1, 1, 1) if root.role == "bot" else (0.9, 0.9, 0.9, 1)
            font_size: 16
            text_size: (self.parent.width - 40, None)
            valign: "top"
            size_hint_y: None
            height: self.texture_size[1] + 20
            pos: (20, 10)


<ChatInterface>:
    orientation: "vertical"
    history: history_rv
    input: input_text

    canvas.before:
        Color:
            rgba: (0.2, 0.2, 0.2, 1)
        Rectangle:
            pos: self.pos
            size: self.size

    RecycleView:
        id: history_rv
        data: [{"text": msg["content"], "role": msg["role"]} for msg in root.messages]
        viewclass: "ChatBubble"
        size_hint_y: 1
        RecycleBoxLayout:
            orientation: "vertical"
            size_hint_y: None
            height: self.minimum_height
            spacing: 15
            padding: [10, 10, 10, 10]

    BoxLayout:
        size_hint_y: None
        height: 70
        padding: [10, 5, 10, 5]
        spacing: 10

        TextInput:
            id: input_text
            multiline: False
            on_text_validate: root.send_message()
            background_color: 0.3, 0.3, 0.3, 1
            padding: [15, 10]

        Button:
            text: "Send"
            on_press: root.send_message()
            size_hint_x: None
            width: 80

""")

class ChatBubble(RecycleDataViewBehavior, FloatLayout):
    text = StringProperty()
    role = StringProperty()
    texture_width = NumericProperty(0)

    def on_text(self, instance, value):
        lbl = self.ids.message_label
        lbl.text_size = (self.parent.width - 40, None) if self.parent else (360, None)
        lbl.texture_update()
        self.texture_width = lbl.texture_size[0]
        self.height = lbl.texture_size[1] + 40

class ChatInterface(BoxLayout):
    history = ObjectProperty()
    input = ObjectProperty()
    messages = []

    def send_message(self):
        message = self.input.text.strip()
        if not message:
            return

        payload = {
            "model": "gpt-3.5-turbo",
            "messages": self.messages + [{"role": "user", "content": message}]
        }
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {OPENAI_API_KEY}"
        }

        try:
            response = requests.post(OPENAI_API_URL, json=payload, headers=headers)
            response.raise_for_status()
            completion_data = response.json()
            completion_message = completion_data["choices"][0]["message"]["content"]
            assistant_role = completion_data["choices"][0]["message"]["role"]

            self.messages.append({"role": "user", "content": message})
            self.messages.append({"role": "bot", "content": completion_message})

            self.history.data = [{"text": msg["content"], "role": msg["role"]} for msg in self.messages]
            self.input.text = ""
            self.history.scroll_to(self.history.children[0])
        except Exception as e:
            print(f"请求异常: {e}")

class ChatApp(App):
    def build(self):
        return ChatInterface()

if __name__ == "__main__":
    ChatApp().run()

关键修改说明

  1. 修正主布局方向
    将ChatInterface的orientation从horizontal改为vertical,让聊天记录区域在上、输入栏在下,符合常规聊天界面结构。

  2. 实现气泡左右对齐

    • 将ChatBubble基类改为FloatLayout,通过pos_hint设置对齐规则:bot消息左对齐、用户消息右对齐
    • 限制气泡最大宽度为容器宽度减去边距,避免过长消息撑满屏幕
  3. 修复文本布局逻辑

    • 动态计算文本所需宽度,自动调整气泡尺寸
    • 基于气泡容器宽度限制文本换行,确保内容不会溢出气泡
  4. 优化视觉细节

    • 给聊天记录容器添加内边距和气泡间距,提升界面层次感
    • 修复OpenAI API请求的角色映射,确保消息角色匹配
    • 更新聊天记录时直接替换整个数据列表,保证界面实时刷新

内容的提问来源于stack exchange,提问作者user21428851

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 08:49:55