You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

PyQt6 QTextEdit无法充分扩展,导致单个片段滚动而非窗口整体滚动的问题

PyQt6 QTextEdit无法充分扩展,导致单个片段滚动而非窗口整体滚动的问题

我明白你遇到的困扰了——每个FormattedSnippet自己出现滚动条,而不是整个窗口作为一个整体滚动,这确实和你想要的OpenAI风格UI不符。让我一步步帮你理清问题和解决办法:

问题根源

  1. SizePolicy设置不当:你给FormattedSnippet的垂直尺寸策略设为了QSizePolicy.Policy.Maximum,这会限制它的高度不会超过自身的sizeHint,而QTextEdit默认的sizeHint并不是根据内容高度计算的,所以布局不会给它足够的空间来完整显示内容。
  2. QTextEdit默认的滚动行为:QTextEdit在内容超出自身可视区域时,会自动显示滚动条并限制自身高度,而不是主动让父布局撑开自己的高度。
  3. 你之前考虑的QLabel其实是支持富文本的(通过setTextFormat(Qt.TextFormat.RichText)),但它确实没法配合QSyntaxHighlighter使用,所以QTextEdit还是更适合你的场景。

解决方案

核心思路是:让每个FormattedSnippet自动适应内容高度,禁用自身的滚动条,把滚动的职责完全交给父容器Window。具体需要做这几个修改:

1. 调整FormattedSnippet的配置

  • 禁用自身的滚动条,避免单个片段出现滚动;
  • 修正尺寸策略,让垂直方向能被父布局充分撑开;
  • 设置文本后,根据内容高度自动调整控件的最小高度,确保布局能正确计算所需空间。

2. 确保Window的滚动区域正确工作

保持WindowsetWidgetResizable(True)设置,这能让内部的布局随窗口大小自适应。

修改后的完整代码

下面是调整后的关键类代码,其他部分(比如Snippet枚举、parse_snippet等)可以保持不变:

from PyQt6.QtWidgets import (QTextEdit, QSizePolicy, QScrollArea, QFrame, QApplication)
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QFont
import sys
from enum import Enum
import re

# 保持你的Snippet类不变
class Snippet:
    class Type(Enum):
        CODE = "CODE"
        PLAINTEXT = "PLAINTEXT"
    def __init__(self, type: Type, text: str):
        self.type = type
        self.text = text

# 修改后的FormattedSnippet类
class FormattedSnippet(QTextEdit):
    def __init__(self, snippet: Snippet):
        super().__init__()
        # 禁用自身的滚动条,把滚动交给父容器
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        # 调整尺寸策略:水平扩展,垂直方向自适应内容
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        self.setReadOnly(True)

        if snippet.type == Snippet.Type.CODE:
            language, code = parse_snippet(snippet.text)
            self.setText(code)
            font = QFont("Courier New")
            font.setStyleHint(QFont.StyleHint.Monospace)
            self.setFont(font)
            self.setStyleSheet(
                """
                QTextEdit {
                    background-color: #000000;
                    color: #dcdcdc;
                    border-radius: 4px;
                    padding: 4px;
                }
                """
            )
        else:
            self.setText(snippet.text)
            self.setStyleSheet(
                """
                QTextEdit {
                    background-color: transparent;
                    color: #E0E0E0;
                    border: none;
                    padding: 4px;
                }
                """
            )
        
        # 关键:设置文本后,计算内容高度并调整控件最小高度
        self.adjust_to_content()
    
    def adjust_to_content(self):
        # 让文档宽度和控件视口宽度一致,确保换行正确计算
        doc = self.document()
        doc.setTextWidth(self.viewport().width())
        # 获取内容的总高度,加上padding的8px(上下各4px)
        content_height = doc.size().height()
        # 设置最小高度,确保布局能给控件足够空间
        self.setMinimumHeight(int(content_height) + 8)
    
    # 可选:如果窗口大小变化,重新调整内容高度(比如窗口变窄,文本换行后高度变化)
    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.adjust_to_content()

# 保持你的parse_snippet和extract_snippet不变
def parse_snippet(snippet: str):
    """ Parse a fenced code block: ```lang code here ``` Returns (language, code). """
    pattern = re.compile(r"```(\w+)\s*(.*?)\s*```", re.DOTALL)
    match = re.search(pattern, snippet.strip())
    if match:
        lang, code = match.groups()
        return lang.strip(), code.strip()
    return "text", snippet # fallback

def extract_snippets(document: str) -> list[Snippet]:
    # Split the document by code fence markers
    parts = re.split(r"(```\w+[\s\S]*?```)", document)
    snippets = []
    for part in parts:
        if part.strip():
            if part.startswith("```") and part.endswith("```"):
                snippets.append(Snippet(Snippet.Type.CODE, part))
            else:
                snippets.append(Snippet(Snippet.Type.PLAINTEXT, part))
    return snippets

# 保持Window类不变,确认滚动区域配置
class Window(QScrollArea):
    def __init__(self, test_doc):
        super().__init__()
        self.setWidgetResizable(True)
        frame = QFrame()
        layout = QVBoxLayout(frame)
        frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.setWidget(frame)

        snippets = extract_snippets(test_doc)
        for snippet in snippets:
            formatted_snippet = FormattedSnippet(snippet)
            layout.addWidget(formatted_snippet)

if __name__ == "__main__":
    test_doc = """Here is some text Here is some text Here is some text Here is some text Here is some text
```python
def hello(document: str):
    print("world")
    # Prints 'world'
    print("world")
    a = "hello"
    print(a)
def test():
    return

More text here

import java.util.Random;
public class RandomSnippet {
    public static void main(String[] args) {
        // Generate a random element from an array
        String[] colors = {"red", "green", "blue"};
        int randomIndex = new Random().nextInt(colors.length);
        String randomColor = colors[randomIndex];
        System.out.println("Random color: " + randomColor);
        // Example of generating multiple random numbers
        System.out.println("Generating 5 random numbers:");
        for (int i = 0; i < 5; i++) {
            System.out.println(new Random().nextInt(100)); // Generates random numbers between 0 and 99
        }
    }
}

Final text"""
test_doc *= 1
app = QApplication([sys.argv[0]])
window = Window(test_doc)
window.resize(800, 600)
window.show()
sys.exit(app.exec())

### 额外说明
- `adjust_to_content`方法会根据文本内容计算所需高度,确保控件能完整显示所有内容;
- 重写`resizeEvent`是为了应对窗口宽度变化的情况——当窗口变窄,文本自动换行后,内容高度会增加,这时候重新计算高度能保证控件依然完整显示内容;
- 禁用滚动条后,QTextEdit就不会再自己限制高度,父布局会把所有片段的高度累加,让整个`Window`的滚动区域来承载所有内容,实现你想要的整体滚动效果。

内容来源于stack exchange

火山引擎 最新活动