PyQt6 QTextEdit无法充分扩展,导致单个片段滚动而非窗口整体滚动的问题
PyQt6 QTextEdit无法充分扩展,导致单个片段滚动而非窗口整体滚动的问题
我明白你遇到的困扰了——每个FormattedSnippet自己出现滚动条,而不是整个窗口作为一个整体滚动,这确实和你想要的OpenAI风格UI不符。让我一步步帮你理清问题和解决办法:
问题根源
- SizePolicy设置不当:你给
FormattedSnippet的垂直尺寸策略设为了QSizePolicy.Policy.Maximum,这会限制它的高度不会超过自身的sizeHint,而QTextEdit默认的sizeHint并不是根据内容高度计算的,所以布局不会给它足够的空间来完整显示内容。 - QTextEdit默认的滚动行为:QTextEdit在内容超出自身可视区域时,会自动显示滚动条并限制自身高度,而不是主动让父布局撑开自己的高度。
- 你之前考虑的QLabel其实是支持富文本的(通过
setTextFormat(Qt.TextFormat.RichText)),但它确实没法配合QSyntaxHighlighter使用,所以QTextEdit还是更适合你的场景。
解决方案
核心思路是:让每个FormattedSnippet自动适应内容高度,禁用自身的滚动条,把滚动的职责完全交给父容器Window。具体需要做这几个修改:
1. 调整FormattedSnippet的配置
- 禁用自身的滚动条,避免单个片段出现滚动;
- 修正尺寸策略,让垂直方向能被父布局充分撑开;
- 设置文本后,根据内容高度自动调整控件的最小高度,确保布局能正确计算所需空间。
2. 确保Window的滚动区域正确工作
保持Window的setWidgetResizable(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




